Repository navigation
Native chat: say when Codex is signed out or the agent isn't installed, without disabling Send - #25666
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughClaude and Codex catalog probes now identify signed-out accounts and missing executables. Catalog reads expose expiring availability observations while preserving cached model listings. Account kind is carried through structured session failures and provider-specific messages. Native chat uses availability to block sending, present guidance, and adjust duplicate start-failure and retry notices. Priority: ⬇️ Low Merge Risk: 🟡 Moderate · up to Run the required real Claude CLI tests before merging. The reported automated checks do not validate this changed startup path against a real CLI. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The inspected changes preserve existing account isolation and do not show a new security issue. Remaining uncertainty concerns compatibility and side effects across supported software versions, rather than a confirmed authorization bypass. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Important
The new account-aware sign-in copy is lost on one path: a message the host records and then rejects drops its account before the desktop words it, so a managed account is shown the system instructions. Details below.
Reviewed changes
- Composer availability gate — the host catalog's
unavailabledisables Send (click, keyboard, and dispatch) and explains itself in the Send tooltip; the send-state extraction intonative-chat-composer-send-state.tsis behavior-preserving. - Host-side evidence — the Claude and Codex model probes now report signed-out / missing-CLI as typed
AgentModelCatalogUnavailableErrors; the store keeps that evidence for 30s under an origin-keyed refresh and the service surfaces it as an optionalunavailablefield. - Account-aware failure copy —
notSignedInnow selects managed/system Claude and Codex instructions, withaccountcarried through refusal details, the failure fact, and the durable refusal. - Hidden automatic rows — desktop filters signed-out / missing-CLI start-failure rows and the automatic launch Retry line, leaving the unsent message's own rejection reason.
⚠️ Managed-account sign-in copy is dropped on the outbox copy of a rejection
structuredAgentSessionRejectedFailure reads the whole rejection fact (which now carries account) but rebuilds it as { kind, attachment }, and StructuredAgentSessionRejectionFact picks only 'kind' | 'attachment'. When the desktop words a rejected message before its journal row loads, structuredAgentSessionAttemptFailureParts has no recorded fact and notSignedIn is not in WORDED_FROM_WHOLE_FACT, so agentSessionSignInCopyId(provider, undefined) chooses the system copy. A managed Claude or Codex account is therefore told to "Run claude and sign in with /login…" instead of "This account isn't signed in. Sign in again in Claude/Codex Accounts settings." — the exact distinction this PR adds.
Technical details
# Managed account loses its sign-in copy on the outbox rejection
## Affected sites
- `src/shared/structured-agent-session-outbox.ts:54` — `StructuredAgentSessionRejectionFact` picks only `'kind' | 'attachment'`, omitting the new `account`.
- `src/shared/structured-agent-session-outbox.ts:67` — `structuredAgentSessionRejectedFailure` reads `account` via `readWholeAgentSessionFailureFact` then rebuilds `{ kind, attachment }`, discarding it.
- `src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.ts:132` — `deliveryNoticeText` calls `structuredAgentSessionAttemptFailureParts(entry.lastFailure, context)` with no `recorded` fact.
- `src/shared/structured-agent-session-send-disposition.ts:191` — with no `recorded`, `fact = failure.rejection` is account-less; `:193` only short-circuits for `providerRejected`/`startFailed`/`restartFailed`, so `notSignedIn` falls through to `:196`.
- `src/shared/agent-session-failure-words.ts:208` — `agentSessionSignInCopyId(provider, fact.account)` with `account === undefined` returns the `*SystemNotSignedIn` key.
## Required outcome
A managed account's rejected-message notice must show the managed sign-in copy, matching what `hostRejectionNoticeText` shows once the journal row loads and what the disabled Send tooltip shows.
## Suggested approach (optional)
Add `account` to `StructuredAgentSessionRejectionFact`'s `Pick` and to the object rebuilt in `structuredAgentSessionRejectedFailure`; or add `notSignedIn` to `WORDED_FROM_WHOLE_FACT` so the host's already-correct `failure.reason` sentence is printed when the whole fact is unavailable.
## Reachability
A `notSignedIn` start failure rejects the queued submission with the account-bearing fact (`structured-agent-session-failure-text.ts:70`, `structured-agent-session-start-failure-row.ts`), the reconcile/`disposeStructuredAgentSessionSendResult` stores it via `structuredAgentSessionRejectedFailure`, and desktop draws it through `deliveryNoticeText` until the journal row loads.deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| } | null>(null) | ||
| // oxlint-disable-next-line react-doctor/effect-needs-cleanup -- The replaceable expiry handle is cleared before rearming and by the returned cleanup. | ||
| useEffect(() => { | ||
| if (!enabled || !optionCatalog || (agent !== 'claude' && agent !== 'codex')) { |
There was a problem hiding this comment.
This early return skips clear(), and the cleanup only cancels the expiry timer, so observation survives a hide. Since waitKey does not change on hide/show, the first render after a hidden pane is revealed re-applies the stale blocker (and its stale account) before the passive effect clears it. Calling clear() here (or resetting observation in the cleanup) keeps the 30s lifetime enforced across a hide.
Re-applies the send gate onto main's catalog store, which now keys in-flight listings by the lister (probe vs a chat's own connection) and lets a waiting picker answer from whichever catalog lands first: - Availability evidence moves to agent-model-catalog-availability.ts; only a probe lister sets or clears it, derived from the lister instead of an origin key. - A waiting picker with no catalog also answers once the account is known signed out or missing its CLI; with a catalog it waits on the probe, and a catalog that lands before the account check reports the check still running so the renderer waits once more. - The background Codex catalog refresh records its failures as a live listing. - Main's API-key rule for Claude moves into the shared signed-out check, so the probe never disables Send for an API-key user.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
8e20d6ea-8334-4b51-b8d5-160fd9ea9976
📒 Files selected for processing (50)
src/main/claude/claude-model-catalog-probe.test.tssrc/main/claude/claude-model-catalog-probe.tssrc/main/claude/claude-structured-init-proof.tssrc/main/claude/claude-structured-launch-resolution.tssrc/main/claude/claude-structured-session-acquisition.tssrc/main/codex/codex-model-catalog-probe.test.tssrc/main/codex/codex-model-catalog-probe.tssrc/main/codex/codex-structured-background-catalog.tssrc/main/codex/codex-structured-session-options-catalog.test.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-availability.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-live-availability.test.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-service.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-store.test.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-store.tssrc/main/native-chat/agent-session-wire/structured-agent-session-failure-text.tssrc/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.tssrc/main/native-chat/agent-session-wire/structured-conversation-command.test.tssrc/main/runtime/claude-structured-session-integration.test.tssrc/main/runtime/orca-runtime-get-worktree-ps.tssrc/main/runtime/rpc/methods/structured-agent-session-options-read.test.tssrc/main/runtime/structured-agent-model-catalog-wiring.tssrc/main/text-generation/commit-message-model-discovery.tssrc/renderer/src/components/native-chat/NativeChatComposer.tsxsrc/renderer/src/components/native-chat/NativeChatComposerActions.test.tsxsrc/renderer/src/components/native-chat/NativeChatComposerActions.tsxsrc/renderer/src/components/native-chat/NativeChatLaunchRetry.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/components/native-chat/native-chat-composer-types.tssrc/renderer/src/components/native-chat/structured-agent-session-message-projection.availability.test.tssrc/renderer/src/components/native-chat/structured-agent-session-message-projection.tssrc/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsxsrc/renderer/src/components/native-chat/use-host-model-catalog-upgrade.tssrc/renderer/src/components/native-chat/use-structured-agent-session-options.tssrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/renderer/src/i18n/en-runtime-required.jsonsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/shared/agent-session-availability.test.tssrc/shared/agent-session-availability.tssrc/shared/agent-session-failure.tssrc/shared/agent-session-refusal-details.tssrc/shared/agent-session-refusal-notice.test.tssrc/shared/agent-session-wire.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.ts
- src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts
- src/renderer/src/i18n/en-runtime-required.json
- src/renderer/src/i18n/locales/fr.json
- src/renderer/src/i18n/locales/zh.json
- src/renderer/src/i18n/locales/ko.json
- src/renderer/src/i18n/locales/es.json
- src/renderer/src/i18n/locales/en.json
- src/renderer/src/i18n/locales/ja.json
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.
| initialization: unknown, | ||
| accountKind?: AgentSessionAccountKind | ||
| ): AgentSessionAcquisitionRefusal | null { | ||
| const account = | ||
| isRecord(initialization) && isRecord(initialization.account) ? initialization.account : {} | ||
| // An API key (ANTHROPIC_API_KEY or a Console /login key) reports tokenSource "none". | ||
| const apiKeySource = readClaudeFrameString(account, 'apiKeySource') | ||
| return readClaudeFrameString(account, 'tokenSource') === 'none' && | ||
| (apiKeySource === null || apiKeySource === 'none') | ||
| return claudeInitializationSignedOut(initialization) | ||
| ? new AgentSessionAcquisitionRefusal( | ||
| 'Claude is not signed in for the selected account. Sign in with the Claude CLI for this CLAUDE_CONFIG_DIR, then retry.', | ||
| 'notSignedIn' | ||
| 'notSignedIn', | ||
| accountKind |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
git diff --name-status b922342199bb4c816072deb42cc58cf755420488 70502f6ecfd3cc7ff16dedf171bad2f931d094f1 -- 'src/main/claude/claude-structured-*' && git diff --unified=5 b922342199bb4c816072deb42cc58cf755420488 70502f6ecfd3cc7ff16dedf171bad2f931d094f1 -- src/main/claude/claude-structured-init-proof.tsRepository: stablyai/orca
Length of output: 2136
🏁 Script executed:
if ! command -v gh >/dev/null 2>&1; then
echo "gh CLI unavailable"
exit 0
fi
numbers=$(gh api repos/stablyai/orca/commits/70502f6ecfd3cc7ff16dedf171bad2f931d094f1/pulls --jq '.[].number') || exit $?
if [ -z "$numbers" ]; then
echo "No associated pull request returned for reviewed commit"
exit 0
fi
while IFS= read -r number; do
[ -n "$number" ] || continue
gh api "repos/stablyai/orca/pulls/$number" --jq '{number: .number, title: .title, body: .body}' || exit $?
done <<EOF
$numbers
EOFRepository: stablyai/orca
Length of output: 7991
Run the required real Claude CLI tests.
This PR changes files under src/main/claude/claude-structured-*, and its description states that no real Claude CLI was run. Run ORCA_REAL_CLAUDE_CLI_TEST=1 pnpm test src/main/claude/claude-structured-real-cli.test.ts src/main/claude/claude-structured-real-cli-fold.test.ts.
Source: Coding guidelines
…ure record The probe's "signed out" / "CLI missing" verdict now lives on the account's existing model-catalog failure record instead of two extra maps, with the failure TTL as its only lifetime: - Only the probe sets or clears the verdict; a chat's own model listing (which works while signed out) never clears or renews it. An aged-out verdict makes the next read re-probe, however fresh the catalog. - A healthy fresh catalog is no longer re-probed every 30 s; it keeps the normal 10 min fresh window. A Claude start refused as signed out ages the catalog so the next read re-probes. - A waiting picker again answers from whichever catalog lands first and never waits on a probe. A read that still owes the probe's verdict says so, and the composer waits for it once with the new optional waitForAvailability param. - The open-time start-failure line and its transcript row are hidden only while Send is disabled for the same reason; otherwise they show as before. - A host verdict lifetime longer than the client's hold is clamped, not dropped. The catalog read watches only the account settings and the paired host's contact instead of every store write.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
0e672af1-110f-4777-a4af-4136f1818572
📒 Files selected for processing (35)
src/main/claude/claude-model-catalog-probe.test.tssrc/main/claude/claude-model-catalog-probe.tssrc/main/claude/claude-structured-session-acquisition.tssrc/main/claude/claude-structured-session-startup.test.tssrc/main/claude/claude-structured-session-startup.tssrc/main/claude/claude-structured-session-test-support.tssrc/main/codex/codex-model-catalog-probe.test.tssrc/main/codex/codex-model-catalog-probe.tssrc/main/codex/codex-structured-session-options-catalog.test.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-failures.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-live-availability.test.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-service.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-store.test.tssrc/main/native-chat/agent-model-catalog/agent-model-catalog-store.tssrc/renderer/src/components/native-chat/NativeChatLaunchRetry.tsxsrc/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/components/native-chat/native-chat-start-failure-presentation.tssrc/renderer/src/components/native-chat/structured-agent-session-delivery-notices.tssrc/renderer/src/components/native-chat/structured-agent-session-message-projection.availability.test.tssrc/renderer/src/components/native-chat/structured-agent-session-message-projection.tssrc/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsxsrc/renderer/src/components/native-chat/use-host-model-catalog-upgrade.tssrc/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.tssrc/renderer/src/components/native-chat/use-structured-agent-session-messages.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-messages.tssrc/renderer/src/components/native-chat/use-structured-agent-session-start-failure-facts.test.tssrc/renderer/src/components/native-chat/use-structured-agent-session-start-failure-facts.tssrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/shared/agent-session-availability.test.tssrc/shared/agent-session-availability.tssrc/shared/rpc-contract/structured-agent-session-params.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.
| session, | ||
| facts: readClaudeStartupFacts({ | ||
| account: launch.account, | ||
| catalogAccess: session.catalogAccess, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
git diff --name-status b922342199bb4c816072deb42cc58cf755420488 4560f220f604b2c445f6df55d4d8e9c117074ec4 && printf '\\n--- targeted diff ---\\n' && git diff --unified=12 b922342199bb4c816072deb42cc58cf755420488 4560f220f604b2c445f6df55d4d8e9c117074ec4 -- src/main/claude/claude-structured-session-acquisition.tsRepository: stablyai/orca
Length of output: 7710
🏁 Script executed:
if command -v gh >/dev/null 2>&1; then gh pr list --repo stablyai/orca --state all --search 4560f220f604b2c445f6df55d4d8e9c117074ec4 --json number,title,body,url; else printf 'gh CLI unavailable\\n'; fiRepository: stablyai/orca
Length of output: 8015
Run the real Claude CLI tests before merge.
The PR changes Claude structured-session startup code and reports that no real Claude CLI was run. The path instruction requires this test command:
ORCA_REAL_CLAUDE_CLI_TEST=1 pnpm test src/main/claude/claude-structured-real-cli.test.ts src/main/claude/claude-structured-real-cli-fold.test.tsSource: Coding guidelines
…ilure - The sign-in/CLI verdict is gated once where the session hands it to the composer: while a turn runs it neither shows nor refuses, so a follow-up queues as before; goal mode is held by the same gate. The start-failure line and row hide only while that same value is up. - A start whose CLI was never found (a direct spawn's ENOENT, or the one a provider supervisor reports) is now a `cliMissing` failure with the disabled Send's sentence, instead of a generic "couldn't start" beside it. An older client shows the host's sentence for the new kind. - Signed-out words for a slash command keep "Run /x again". - The Claude catalog reads its models only from the reply to its own list_models request, never from the initialize reply. - The Codex probe lists models while its account check runs; a signed-out verdict still wins. - The missing-executable check moves to provider-process, out of native chat.
- Before the catalog probe's read-only app-server reads Orca's shared Codex home, it runs the same sync a launch and the usage poll run, so a `codex login` made in ~/.codex is seen at once instead of leaving "Run codex login" up until something else re-syncs (forever on a headless host). Per-account homes are left alone. - A "no account" answer counts as signed out only when the home's auth.json is absent or holds no credential; one caught mid-write by a token refresh, or holding a credential, is unknown.
…ces it - No gap while signed out: the host serves an aged verdict while the probe that re-derives it runs, and the composer never clears its last answer before a read; only an answer without a verdict, an error, lost contact or an account change does. Send stays disabled with its reason through every re-check. - After sign-in the stale "isn't signed in" start-failure line stays hidden: a probe that found the account fine now says so (`accountVerified`), and the line shows only when the host has no answer at all. - A Claude start refused as signed out, or any start whose CLI was never found, is recorded as that home's verdict, and the chat reads it once when that start failure arrives. - The re-probe mark is read only where the catalog read starts its probe, so a Codex chat's own picker no longer re-lists for it on every read. - Choosing an account or signing in again in Accounts settings re-derives the verdict at the next read; the account key now compares settings by value. - A re-read that brings an identical catalog keeps the option state, so focus re-reads re-render nothing.
…blocks this chat A chat is pinned to the Codex home it started in, so choosing another account in Codex Accounts settings never unblocks it. The system-account sentence now says only to run `codex login`, in every locale.
The sign-in verdict lived inside the model catalog's failure records, so the store needed guards for who wrote a failure, which listing may clear it, and whether a verdict was served while re-probing. Each guard fixed one way a chat's own listing or an older probe overwrote the account's answer. The answer now lives in AgentAccountStatuses: one in-memory, capped status per account home (ready, not signed in, or CLI missing). Only the session-less probe, a refused start and an account change write it. A blocked answer older than 30 s, or one an account change marked, makes the next read start the probe whatever else is listing, and it is served until that probe answers. Older evidence never replaces newer. The catalog's failure code is main's again, and agent-model-catalog-failures.ts is gone. The wire carries one optional `availability` field in place of `unavailable` and `accountVerified`. The renderer holds its answer per host, agent and chat, re-reads only on that agent's account inputs, and re-reads on a timer only while blocked. After an account change, Send stays disabled with the previous words until the re-probe answers, then changes once. Account switches and re-sign-ins mark the status before the settings change a chat's read follows.
…2025/native-chat-send-gate # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-refusal.ts # src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts # src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts # src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx # src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx # src/shared/agent-session-refusal-details.ts # src/shared/agent-session-write-failure.ts
…e mid-probe, and back off while blocked Round-4 review findings on the account status owner: - An account switch or re-sign-in made while a probe was running left the old answer up for up to 30 s: the next read joined that probe, whose answer was then dropped as older than the change. A read now chains exactly one fresh probe after a probe that began before the change. - Removing the selected Claude account swapped another sign-in into the shared config dir without marking the answer; it now marks it first. - Evidence was ordered by wall-clock time, so a clock that stepped back froze a blocked answer. It is now ordered by a sequence and aged on a monotonic clock; a negative age reads as due. - A chat left open while signed out started a CLI probe every 30 s. Probes that keep finding the same block now space out (30 s, 60 s, 2 min, 4 min, then every 5 min), carried to the client as recheckInMs. An account change or a different refusal starts the hold over. - The renderer dropped no settled wait from its leave set; it now does. - A failed Claude probe logged its whole stdout, which now includes the initialize reply naming the account. Control replies are dropped from the log and the failure message. - The account services take the status re-check as an injected onSignInChanged hook instead of importing the native-chat store. - Imports moved below file headers; comments no longer say "ready" means re-checked after the failure.
…2025/native-chat-send-gate
…2025/native-chat-send-gate
…the hook was added
…2025/native-chat-send-gate
…2025/native-chat-send-gate
…igned out or missing
What you see: in a native Claude or Codex chat whose CLI is signed out, or not installed on the
computer running the chat, Send is disabled and its tooltip (hover or keyboard focus) says how to
fix it, in words that match the account ("Codex isn't signed in. Run `codex login`.", "This
Claude account isn't signed in. Sign in again in Claude Accounts settings.", "Claude isn't
installed on the computer running this chat."). A message refused after sending gets the same
words. On desktop the open-time "Chat could not be started" line, its Retry and its transcript row
no longer repeat these two reasons, unless a message of yours is still waiting on that start.
An unknown answer never disables Send, and while a turn runs Send stays Stop and Enter queues.
How: the host's existing session-less model-catalog probe also says why no chat can start.
Claude's probe child sends initialize before list_models and applies the rule chat start already
uses (token source none with no API key); Codex's probe asks account/read beside model/list; a
spawn that finds no executable is "CLI missing". The probe's failure record in the catalog store
carries that reason, and every catalog reply carries it as an optional `unavailable` field. Only
a probe writes or clears it: a live chat's own listing (which a signed-out Codex still answers)
never does. It lasts the store's 30 s failure lifetime; after that the next read starts or joins
the probe, serving the held reason meanwhile, and only a read that already asks to wait gets the
probe's answer. A change to the managed-account settings expires it. Before probing the shared
Codex mirror home, the probe runs the same sync a launch does. The renderer keeps the latest
answer until the next one replaces it, and only while blocked re-reads on window focus and when
the chat gains focus. No status store, timers or new request params.
…2025/native-chat-send-gate # Conflicts: # src/main/runtime/structured-agent-session-runtime.ts # src/renderer/src/components/native-chat/NativeChatComposerActions.tsx # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx # src/renderer/src/components/native-chat/native-chat-composer-types.ts
…2025/native-chat-send-gate # Conflicts: # src/main/claude/claude-structured-launch-resolution.ts # src/main/claude/claude-structured-session-startup.ts # src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.ts
…nd plain keyboard focus on the disabled Send A chat whose account is signed out and has no model list no longer shows the model picker as loading each time it re-checks the sign-in after 30 s: the re-check is for Send, so a reply that already carries the verdict ends the picker's wait. The disabled Send's wrapper takes keyboard focus (so its tooltip opens) without becoming a second button; the inner button keeps its label. Trims tests that repeated rules other tests already prove.
…2025/native-chat-send-gate # Conflicts: # src/main/claude/claude-structured-launch-resolution.ts # src/main/runtime/structured-agent-account-home.ts # src/renderer/src/components/native-chat/NativeChatComposer.tsx # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx # src/renderer/src/components/native-chat/agent-session-failure-words-text.ts # src/renderer/src/components/native-chat/native-chat-structured-session-notices.ts
…hile a queued card is about to send The composer shows Stop while the host's queue is about to send its next card, but the sign-in gate stayed on in that window, so Enter was refused with no message.
Review summary (replaces the earlier summary, which described a version that disabled Send)The problemNative Claude and Codex chats gave no warning when the agent couldn't run. You typed, pressed Send, and only then got an error. Claude's error said "Sign in first." without saying how; Codex had no sign-in check at all. An earlier version of this PR disabled Send when a background check said "not signed in" or "not installed". A review of that version found the check can be wrong while the chat would work (saved Arguments and project-folder credentials never reach the check; Codex itself sends signed-out chats; some proxy setups report "no login"; and Send stayed disabled up to 30 seconds after signing in). So it was rebuilt. What changes for you
Review
What was verified
Not verified
|
…dict; show a dismissible notice instead The host's pre-send check can be wrong while the chat would work (saved Arguments it never sees, project settings it can't read, gateways that accept a signed-out Codex), and a disabled Send left no way past it. - Delete the Send gate: the composer, Enter and goal mode no longer read the verdict, and the Send tooltip reason and focusable wrapper go back to main. - Show Codex not-signed-in and Claude/Codex not-installed as one entry in the chat's existing notice card. Dismissed per verdict; shows again when the verdict changes or clears and returns; steps aside when the failed start (launch line or latest failed-start row) says the same. - Claude: drop the probe's signed-out verdict (initialize-on-probe stdin, request-id filtering, control-reply log scrubbing). The chat start's refusal, with its account kind, is the only Claude sign-in signal. Missing-CLI from the probe stays. - Restore the failed-start line and transcript row for these causes (no more hiding on desktop) and drop the chat-root focus re-check; a window-focus re-read stays while a notice is showing.
…eep a signed-out Codex's model list
Review round 1 of the notice rework.
- A chat that proves its start (at acquire, or on its later "started"
event) marks the catalog's held reason for that account due, so the
next read re-probes instead of trusting it until the TTL; the chat
re-reads when a turn starts or ends while a notice shows.
- The Codex probe keeps a model list it got beside a signed-out reason,
so a gateway user with a wrong verdict keeps the picker list.
- Failure records move to their own module; an expired failure with no
reason is dropped, and expiry drops reason-less failures.
- Notice: any verdict change resets the dismissal; signed-out is
Codex-only; the say-once rule ignores a failed start a turn has run
past.
- Missing-CLI copy: "{{agent}} wasn't found on the computer running this
chat. Install it, or check its Command in Settings → Agents." (6
locales).
…n; drop the unreachable Claude filter Review round 2 of the notice rework. - A re-attach to a live child proved nothing new, so only an attach that acquired a child which proved its start marks the reason due. - Reword: a start proves the program exists; for a Codex sign-in it only prompts an earlier re-check. - Drop the notice's Claude signed-out filter: no host sends that verdict. - Tests: a re-attach does not notify; a turn ending re-reads once more.
… re-checking After signing in to Codex, a new chat flashed the old "isn't signed in" notice for about half a second: past its 30 s lifetime the host answers a plain read with the held reason while its probe re-checks. A chat now adopts such a reason only when it already shows it; otherwise the joined read's answer decides.
…hat-send-gate # Conflicts: # src/main/claude/claude-structured-launch-resolution.ts # src/main/runtime/orca-runtime-get-worktree-ps.ts # src/main/runtime/structured-agent-session-runtime.ts # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx # src/renderer/src/components/native-chat/native-chat-structured-session-notices.ts # src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.ts # src/renderer/src/i18n/en-runtime-required.json
…in words after the merge
…me project with the other SQLite host tests
Resolve against #25666 (signed-out / missing-CLI notice): a proven start still tells the model catalog to re-check its held reason. - Attach: an acquire mints the startup attempt, so the published block notes a start the acquire already proved (phase ready); a re-attach notes nothing. - An accepted `started` readies the attempt, wakes delivery, then notes it. A stale child's `started` notes nothing. - Main's persistStartedOptions is dropped: persistReportedOptions writes the same fields after delivery, only while the report is the newest word. - endReleasedChild moves beside the post-acquire failure it serves, keeping the attach file within its line limit.
…del-catalog-all-agents Brings in #25666 (a probe's "not signed in" / "CLI missing" verdict on the catalog's failure record). Both sides kept: - Store: the verdict rides only discovery listings; a chat's live listing touches neither a plain failure nor a verdict. A superseded probe answer still settles the verdict. The listing-to-entry builder moves to the entry module to keep the store under its line budget. - Service: every answer carries the verdict; a verdict past its TTL is re-probed through the same host-lifetime listing prewarm uses, and prewarm counts an aged verdict as due. - Claude probe keeps its own list_models runner; a spawn whose executable is missing now says so, and the probe turns that into `cliMissing`. - Codex probe registration passes the account kind and launch-home sync. - Renderer: the verdict is reported without bringing back the picker wait.

ELI5
When Claude or Codex can't run because you're signed out or the program can't be found, a native chat let you type and press Send, and only then failed. Claude's message after the fact said "Sign in first." without saying how, and Codex said nothing about sign-in at all before you sent.
Now, when the computer running the chat has found that Codex isn't signed in, or that the Claude or Codex program can't be found, the chat shows a notice saying so and how to fix it, with an X to dismiss it. Send is never disabled by that check: the check can be wrong while the chat would actually work (details below), so you can always try. If a send does fail for one of these reasons, the failure stays on screen in clearer words, and the same reason is not said twice.
What Changed
What you see
codex login."claudeand sign in with /login, or choose an account in Claude Accounts settings."How it works
Orca already runs a short-lived check on the computer that runs the chat to list the agent's models. That check now also reports a typed reason when it can tell the agent can't run, carried as a new optional field on the model-list reply the chat already reads:
codex logindone in another terminal is seen.Why not disable Send
An earlier version of this PR disabled Send while the check said "not signed in" or "not installed". It was dropped because the check can be wrong while the chat would work, and then Send would stay disabled with no way past it:
.claude/settings.jsoncredentials are invisible to it. A per-account check can't answer a per-folder question.A notice you can dismiss tells you early when the check is right and costs nothing when it is wrong. This PR deletes the Send gate entirely (the gate, its Enter and goal-mode checks, the Send tooltip reason, the hidden-line rule for these two causes, and the Claude startup probe), so it is much smaller than the previous version.
Differences from the common pattern
The common pattern shows a dismissible provider-status error at the top of the chat for "Codex not signed in" and "program not found", keyed by the message so a changed status shows again, never disables Send for it, and has no pre-send Claude sign-in check. This PR follows that mechanism. Differences:
orcad) doesn't mark reasons stale on an account change; the 30-second lifetime corrects it.Linked Issue
Maintainer work; no linked issue.
Visual Proof
Hidden-window QA on Windows, build
058f39269b0, stand-in Claude and Codex (never the real programs), real config files unchanged before and after.codex login(stand-in signed in): cleared, next send answeredRe-check on the fixed build
8a5045f7ac9(same machine and setup):Testing
I manually tested these changes (hidden-window QA on Windows with stand-in agents; see Visual Proof)
Automated tests added/updated
New: the notice shows for Codex signed out and for a missing Claude or Codex, Send still sends with a verdict present, dismissal holds for the same verdict and resets on any change, the notice steps aside when the failed start or the transcript's latest failed-start row states the same reason (but not a different one, and not once a turn has run since), the Codex check keeps its model list beside a signed-out reason, a chat proving its start (at acquire, or by its later "started" event) marks the reason stale so the next read re-checks while a reconnect to a running chat does not, the chat re-reads when a turn starts and when it ends, and expired reason-less failures are dropped. The Claude check sends only the model-list request and reports no sign-in verdict.
Rules confirmed by removing each one and seeing its test fail (14): a reason the host is still re-checking is never newly shown (the flash QA found); start-proof at acquire tells the catalog; only an acquiring attach counts, not a re-attach; start-proof on the "started" event tells the catalog; the catalog marks the reason stale on a start; the chat re-reads on turn start and end while a notice shows; the store keeps the reason beside a probe's list; the Codex check returns its list when signed out; dismissal resets on any verdict change; a turn after a failed start voids it for say-once; expired reason-less failures are dropped; the say-once rule itself; the notice is wired into the card; the window-focus re-read.
Kept from before: the host rules (only the check sets or clears a reason, 30-second lifetime, account-change staleness, Codex account read, missing-program typing) and the clearer post-send words.
Typecheck (node, web, cli), lint, the changed-code quality gate and the localization checks pass.
The repo's real-Claude test suite passed on the final head
5239f312b73(12 of 12), after the main merge.Manual QA found one bug, now fixed: a brand-new chat opened right after signing in to Codex flashed the old notice for about half a second. The fix has a test that fails without it, and the live re-check on
8a5045f7ac9passed (see Visual Proof).Not verified: "program not found" live (the QA machine has real Claude and Codex installed that Orca would find, so it is covered by unit tests only), the real Codex program, Linux, macOS rendered UI, a remote host over SSH, the phone app.
Agent skill upstream boundary
Notes
Author: @BrennanKB5.
Ensure no issues in: security (the Codex login file is only read for presence, never logged or sent), cross-platform (missing-program detection only trusts a real "file not found"), remote/SSH (the computer that runs the chat answers for itself; an older one simply never sends a reason, so no notice), mobile (unchanged; it ignores the new optional field), backwards compatibility (all new fields are optional), performance (no new processes for healthy chats; while a verdict is held, even after Dismiss, the chat re-reads on window focus and on each turn start and end, but a read is answered from memory and the check itself runs at most once per 30 seconds per account, plus once after each new chat start).
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)