Repository navigation
feat(native-chat): each Claude/Codex chat gets its own permission mode - #26116
Conversation
66d4ff4 to
da54347
Compare
Review and readiness summaryReviews. Eight adversarial review rounds by Codex What the reviews found and fixed, in plain terms:
Live QA. Electron QA on macOS desktop passed every row, including a final round on the final branch. That round covered the new descriptions, Accept edits launching directly, and Full access picked during a reply. Not verified live: a real Codex automatic-review session, Windows/Linux, SSH, and the phone. The mobile paths are covered by hook-level tests only. Tests.
CI. On
CI is re-running on Not changed. Terminal launches and Settings → Agents → Agent Permissions behave as before. Grok and other agents' chats show no button; the follow-up for Grok is STA-9763. |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 10 seconds. View limit details
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to Chats now carry their own Claude and Codex permission mode across desktop and mobile. Two problems remain from earlier review. If Claude fails to start while a permission change is being prepared, queued messages are marked as generic host failures and the real error is hidden. On mobile, the permission control can disappear when no other chat options are loaded. Fix both, or explicitly accept them, before merging. Pre-merge checks |
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 Minor · Show the permission pill even when the option… · use-mobile-native-chat-session-option-controller.ts:87
mobile/src/session/use-mobile-native-chat-session-option-controller.ts:87
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winShow the permission pill even when the option snapshot is empty.
structuredControllerisnullwheneverstructuredSnapshot.length === 0. In that case,nativeChatSessionOptionsisnull, andstructured.permissionPickeris dropped.MobileNativeChatSessionOptionPickersalready returns the permission pill alone when no model descriptor exists, and its test covers this case withsnapshot: []. That path cannot be reached from this controller. A Claude or Codex chat whose host reports permission modes but has no model descriptor yet shows no permission control on mobile.Return picker props when
structured.permissionPickerexists, even if the snapshot is empty.Proposed fix
- activeChatStructured && structuredSnapshot.length > 0 + activeChatStructured && (structuredSnapshot.length > 0 || structured.permissionPicker)Also add
structured.permissionPickerto the memo dependencies.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
d66bd2d8-5df4-405e-88e6-ecad71aba3d3
⛔ Files ignored due to path filters (1)
src/shared/__snapshots__/native-chat-terminal-launch-isolation.test.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (148)
config/scripts/vitest-sqlite-runtime-files.mjsmobile/src/session/MobileNativeChatPermissionPicker.test.tsmobile/src/session/MobileNativeChatPermissionPicker.tsxmobile/src/session/MobileNativeChatSessionOptionPickers.test.tsmobile/src/session/MobileNativeChatSessionOptionPickers.tsxmobile/src/session/MobileNativeChatSessionOptionRows.tsxmobile/src/session/mobile-native-chat-session-option-persistence.tsmobile/src/session/mobile-structured-agent-session-rpc.tsmobile/src/session/mobile-structured-session-controller.tsmobile/src/session/use-mobile-native-chat-controller.tsmobile/src/session/use-mobile-native-chat-session-option-controller.tsmobile/src/session/use-mobile-structured-agent-mutation.tsmobile/src/session/use-mobile-structured-agent-options.test.tsmobile/src/session/use-mobile-structured-agent-options.tsmobile/src/session/use-mobile-structured-agent-permission-refresh.test.tsmobile/src/session/use-mobile-structured-agent-session.tsmobile/src/session/use-mobile-structured-permission-write.test.tsmobile/src/session/use-mobile-structured-session-host-stopping.tssrc/main/claude/claude-agent-sdk-contract-pins.test.tssrc/main/claude/claude-structured-acquisition-launch.tssrc/main/claude/claude-structured-fast-mode-support.tssrc/main/claude/claude-structured-initial-permission-intent.test.tssrc/main/claude/claude-structured-launch-resolution.test.tssrc/main/claude/claude-structured-launch-resolution.tssrc/main/claude/claude-structured-model-catalog.tssrc/main/claude/claude-structured-options-context-window.test.tssrc/main/claude/claude-structured-options.tssrc/main/claude/claude-structured-permission-application.tssrc/main/claude/claude-structured-permission-mode-option.test.tssrc/main/claude/claude-structured-permission-mode.test.tssrc/main/claude/claude-structured-permission-mode.tssrc/main/claude/claude-structured-permission-option.tssrc/main/claude/claude-structured-permission-owner.test.tssrc/main/claude/claude-structured-permission-reconciliation.test.tssrc/main/claude/claude-structured-real-cli.test.tssrc/main/claude/claude-structured-session-acquisition.tssrc/main/claude/claude-structured-session-adapter-turns.test.tssrc/main/claude/claude-structured-session-adapter.test.tssrc/main/claude/claude-structured-session-adapter.tssrc/main/claude/claude-structured-session-options.tssrc/main/claude/claude-structured-session-publication.tssrc/main/claude/claude-structured-session-startup.tssrc/main/claude/claude-structured-session-state.tssrc/main/claude/claude-structured-spawn-options.test.tssrc/main/claude/claude-structured-spawn-options.tssrc/main/claude/claude-structured-start-permission-mode.test.tssrc/main/claude/claude-structured-start-permission-mode.tssrc/main/claude/claude-supervised-stop-real-cli.test.tssrc/main/codex/codex-structured-agent-definition.tssrc/main/codex/codex-structured-launch-resolution.test.tssrc/main/codex/codex-structured-launch-resolution.tssrc/main/codex/codex-structured-permission-mode.test.tssrc/main/codex/codex-structured-permission-mode.tssrc/main/codex/codex-structured-permission-policy.test.tssrc/main/codex/codex-structured-permission-policy.tssrc/main/codex/codex-structured-permission-reconciliation.test.tssrc/main/codex/codex-structured-permission-test-support.tssrc/main/codex/codex-structured-real-cli-visuals.test.tssrc/main/codex/codex-structured-session-acquire.tssrc/main/codex/codex-structured-session-adapter.test.tssrc/main/codex/codex-structured-session-options.tssrc/main/codex/codex-structured-session-state.tssrc/main/codex/codex-structured-thread-open.test.tssrc/main/codex/codex-structured-thread-open.tssrc/main/codex/codex-structured-turn-start.tssrc/main/codex/codex-structured-visuals.test.tssrc/main/codex/codex-structured-visuals.tssrc/main/native-chat/agent-chat-permission-mode-setting.test.tssrc/main/native-chat/agent-chat-permission-mode-setting.tssrc/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.tssrc/main/native-chat/agent-session-wire/agent-session-subscriber-frame-fields.tssrc/main/native-chat/agent-session-wire/structured-agent-definition.tssrc/main/native-chat/agent-session-wire/structured-agent-session-acquire-aborts.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-acquire-aborts.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-child-relaunch.tssrc/main/native-chat/agent-session-wire/structured-agent-session-claude-hung-start-stop.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-claude-permission-order.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-claude-permission-relaunch.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-client-delivery.tssrc/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.tssrc/main/native-chat/agent-session-wire/structured-agent-session-dispatch-preparation.tssrc/main/native-chat/agent-session-wire/structured-agent-session-ended-child-failure.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-types.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host.tssrc/main/native-chat/agent-session-wire/structured-agent-session-options-read.tssrc/main/native-chat/agent-session-wire/structured-agent-session-permission-mode.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-permission-publication.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-provisional-permission.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stopped-permission-intent.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-subscribers.tssrc/main/persistence/loading-store/loaded-chat-permission-setting.test.tssrc/main/persistence/loading-store/loaded-chat-permission-setting.tssrc/main/persistence/loading-store/normalize-loaded-global-settings.tssrc/main/persistence/loading-store/normalize-loaded-profile-state.tssrc/main/runtime/claude-structured-startup-fault-is-not-an-exit.test.tssrc/main/runtime/claude-structured-startup-unanswered-control-request.test.tssrc/main/runtime/orca-runtime-get-structured-agent-session-create-support.tssrc/main/runtime/orca-runtime-get-worktree-ps.tssrc/main/runtime/orca-runtime-structured-agent-session-create-intent.test.tssrc/main/runtime/structured-agent-runtime-registrations.tssrc/main/runtime/structured-agent-session-runtime.tssrc/main/runtime/structured-claude-runtime-adapter.tssrc/renderer/src/components/native-chat/NativeChatComposer.tsxsrc/renderer/src/components/native-chat/NativeChatComposerActions.tsxsrc/renderer/src/components/native-chat/NativeChatComposerField.tsxsrc/renderer/src/components/native-chat/NativeChatPermissionModePicker.test.tsxsrc/renderer/src/components/native-chat/NativeChatPermissionModePicker.tsxsrc/renderer/src/components/native-chat/native-chat-composer-types.tssrc/renderer/src/components/native-chat/native-chat-permission-mode-labels.tssrc/renderer/src/components/native-chat/use-structured-agent-session-option-state.tssrc/renderer/src/components/native-chat/use-structured-agent-session-options.tssrc/renderer/src/components/native-chat/use-structured-agent-session-permission-picker.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/renderer/src/components/native-chat/use-structured-permission-write.test.tsxsrc/renderer/src/components/settings/AgentsPane.tsxsrc/renderer/src/components/settings/ChatPermissionSetting.tsxsrc/renderer/src/components/settings/ChatSettingsSection.navigation.test.tsxsrc/renderer/src/components/settings/ChatSettingsSection.test.tsxsrc/renderer/src/components/settings/ChatSettingsSection.tsxsrc/renderer/src/components/settings/chat-naming-persistence.test.tsxsrc/renderer/src/components/settings/chat-permission-search.tssrc/renderer/src/hooks/settings-navigation-interface-sections.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/renderer/src/lib/structured-agent-session-launch-options.test.tssrc/renderer/src/lib/structured-agent-session-launch-options.tssrc/shared/agent-chat-permission-mode.test.tssrc/shared/agent-chat-permission-mode.tssrc/shared/agent-session-wire.tssrc/shared/default-global-settings.tssrc/shared/global-settings-types.tssrc/shared/native-chat-session-option-defaults.tssrc/shared/native-chat-terminal-launch-isolation.test.tssrc/shared/structured-agent-session-coalescer.tssrc/shared/structured-agent-session-option-picks.test.tssrc/shared/structured-agent-session-option-picks.tssrc/shared/structured-agent-session-options.tssrc/shared/structured-agent-session-permission-coalescing.test.tssrc/shared/structured-agent-session-permission-state.test.tssrc/shared/structured-agent-session-reducer.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.
| const ready = session.startup.state === 'pending' ? session.startup.settled : Promise.resolve() | ||
| return ready.then(async () => { | ||
| if (session.startup.state !== 'proven') { | ||
| throw session.startup.failure ?? new Error('claude startup did not complete') | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
rg -nP -C4 "startup\.(state|failure)" src/main/claude/claude-structured-dispatch.ts src/main/claude/claude-structured-session-startup.ts
rg -nP -C3 "state:\s*'(failed|proven|pending)'" src/main/claudeRepository: stablyai/orca
Length of output: 13488
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- permission application ---'
nl -ba src/main/claude/claude-structured-permission-application.ts | sed -n '1,180p'
printf '%s\n' '--- dispatch references ---'
rg -n -F --glob '*.ts' -- 'dispatchClaudeTurn' src/main/claude src/main | head -80
rg -n -F --glob '*.ts' -- 'prepareClaudePermissionMode' src/main
printf '%s\n' '--- dispatch definitions and relevant callers ---'
rg -n -F --glob '*.ts' -- 'prepareStructuredAgentSessionDispatch' src/main
rg -n -F --glob '*.ts' -- 'StructuredAgentSessionDeliveryLoop' src/main
printf '%s\n' '--- startup state and failure helpers ---'
nl -ba src/main/claude/claude-structured-session-startup-state.ts | sed -n '1,180p'
rg -n -P -C8 "failClaudeStartup|startup\.failure|startup\.state" src/main/claude/claude-structured-dispatch.ts src/main/claude/claude-structured-session-startup.tsRepository: stablyai/orca
Length of output: 31295
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- dispatch preparation ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-dispatch-preparation.ts | sed -n '1,220p'
printf '%s\n' '--- delivery loop ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.ts | sed -n '1,280p'
printf '%s\n' '--- host delivery ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.ts | sed -n '1,180p'
printf '%s\n' '--- Claude session adapter around preparation ---'
nl -ba src/main/claude/claude-structured-session-adapter.ts | sed -n '210,270p'
printf '%s\n' '--- Claude dispatch ---'
nl -ba src/main/claude/claude-structured-dispatch.ts | sed -n '90,280p'Repository: stablyai/orca
Length of output: 34275
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- handover submission ---'
rg -n -F --glob '*.ts' -- 'export async function handOverSubmission' src/main/native-chat/agent-session-wire
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts | sed -n '1,260p'
printf '%s\n' '--- Claude adapter dispatch binding ---'
rg -n -P -C8 "dispatch\s*[:=]|dispatch\(" src/main/claude/claude-structured-session-adapter.ts | head -120
printf '%s\n' '--- adapter contract ---'
rg -n -P -C8 "dispatch\??:" src/main/native-chat/agent-session-wire/structured-agent-session-adapter.tsRepository: stablyai/orca
Length of output: 13259
Do not convert failed Claude startup into a host fault.
When prepareClaudePermissionMode rejects with session.startup.failure, the rejection reaches StructuredAgentSessionDeliveryLoop.run, which records { hostFault: true }. This hides the provider startup diagnostic.
If startup has failed, return without preparing. The next handover dispatch checks claudeStartupFailureFact(session) and records the provider startup failure instead. The early return also prevents repeated preparation attempts.
Proposed fix
--- "a/src/main/claude/claude-structured-permission-application.ts"
+++ "b/src/main/claude/claude-structured-permission-application.ts"
@@ -91,17 +91,21 @@
}
/** Runs outside the mutation lane so Stop can cancel startup or reconciliation. */
export function prepareClaudePermissionMode(
session: ClaudeSession,
timeoutMs: number | undefined
): Promise<void> | undefined {
+ // A failed start is the child's to report; preparation must not turn it into a host fault.
+ if (session.startup.state !== 'pending' && session.startup.state !== 'proven') {
+ return undefined
+ }
if (!claudePermissionNeedsPreparation(session) && !permissionOwners.get(session)?.pending) {
return undefined
}
const ready = session.startup.state === 'pending' ? session.startup.settled : Promise.resolve()
return ready.then(async () => {
if (session.startup.state !== 'proven') {
- throw session.startup.failure ?? new Error('claude startup did not complete')
+ return
}
let preparing = true
while (preparing) {
Review summaryThis PR went through many adversarial review rounds and live QA rounds, each in a hidden, isolated Orca with stand-in agents. Every finding above minor was fixed before the next round. How the design changed during review
Found late and fixed
Verified:
Not verified:
Known limits are listed in the PR body. They are three minor cases plus one temporary follow-up: focus returning to the message box after picking a mode. |
|
Update since the review summary (now at
|
Each Claude and Codex chat now has its own permission mode (Ask for approval, Accept edits, Approve for me, Full access), saved as one more chat option and picked from a button after "+" on desktop or a pill on the phone. The mode takes effect when the agent starts: Claude gets launch flags, Codex gets the thread policy on start and resume. A change restarts an idle agent before the next message, never during a reply. New chats take the host's default from Settings > Chat, seeded once from Agent Permissions.
The restart check, the start and the options read now share one host derivation, so a running agent whose mode differs from what a start would launch is replaced, and the button shows what the host runs. A permission pick shows at once and never locks the picker while its write waits behind a starting agent. One shared table names each agent's modes; the phone shows its permission pill even without a model pill.
…uns Approve for me The menu and the host now call the same function: the chat's listed model row, or the row the CLI default resolves to when the chat names none. The model-catalog probe now marks that default row, as a live listing already does, so a new chat can offer Approve for me before its first message.
…ng lands A chat opened before its account's first model listing read its options at rest from the agent's built-in list, which names no Approve for me support. The client takes that answer as the session's own list, so the host list that landed a moment later was ignored until the first turn. When a waited listing lands, the chat now reads its options again, on desktop and phone.
…mission-modes-v2 # Conflicts: # mobile/src/session/MobileNativeChatSessionOptionPickers.tsx # src/main/claude/claude-structured-acquisition-launch.ts # src/main/claude/claude-structured-launch-resolution.ts # src/main/claude/claude-structured-session-state.ts # src/main/codex/codex-structured-launch-resolution.test.ts # src/main/codex/codex-structured-launch-resolution.ts # src/main/codex/codex-structured-session-acquire.ts # src/main/codex/codex-structured-session-state.ts # src/main/native-chat/agent-session-wire/structured-agent-session-options-read.test.ts # src/main/native-chat/agent-session-wire/structured-agent-session-options-read.ts # src/main/runtime/structured-agent-runtime-registrations.ts # src/main/runtime/structured-claude-runtime-adapter.ts
7e29652 to
f467d20
Compare
|
Rebuilt on the simpler design and force-pushed (head f467d20). The previous version is kept on branch
|
…mission-modes-v2 # Conflicts: # src/main/runtime/structured-agent-runtime-registrations.ts
…#26116) Upstream stablyai#26116 ships the same capability provider-neutrally (via structured-agent-session-permission-mode) and deleted this branch's claude-specific claude-structured-permission-mode.ts + test. Per plan, this branch is retired rather than reconciled line-by-line: the merge resolves to main's tree exactly, dropping the superseded permission-mode implementation. The branch contributes nothing further; integration can treat it as main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… permission-mode code Full access still prompted for every tool. Orca always attaches its own canUseTool permission callback (the SDK pairs it with --permission-prompt-tool stdio), so a bare --dangerously-skip-permissions never stopped the CLI routing each tool back to that callback, which had no bypass path. The callback now auto-approves tool-permission requests when the chat is in full access, while still surfacing genuine questions (AskUserQuestion) and plan proposals (ExitPlanMode). Also drops our superseded "System B" permission-mode code now that upstream's per-chat permission mode (stablyai#26116) is in integration: the provider-neutral report types, the top-level AgentSessionOptionsResult.permissionMode field and its readRestingPermissionMode producer, CLAUDE_ALLOW_BYPASS_FLAG, the dead allowDangerouslySkipPermissions option, the reference doc, and the related AGENTS.md/.gitignore entries. Upstream's System A (AgentChatPermissionMode, current.permissionMode, the picker) is untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
stablyai#26116) Each native Claude and Codex chat now has its own permission mode (Ask for approval, Accept edits for Claude, Approve for me, Full access), shown and changed from a button right after "+" in the composer. A change takes effect before the next message: the agent restarts quietly and resumes the same conversation, never interrupting a reply or background work. New chats start from Settings → Chat → Permissions, which is seeded once from Agent Permissions. The mode is stored as an ordinary chat option, and the restart reuses the existing "replace the agent before the next message" step. (cherry picked from commit 2233fd3)
#26116) Each native Claude and Codex chat now has its own permission mode (Ask for approval, Accept edits for Claude, Approve for me, Full access), shown and changed from a button right after "+" in the composer. A change takes effect before the next message: the agent restarts quietly and resumes the same conversation, never interrupting a reply or background work. New chats start from Settings → Chat → Permissions, which is seeded once from Agent Permissions. The mode is stored as an ordinary chat option, and the restart reuses the existing "replace the agent before the next message" step. (cherry picked from commit 2233fd3)
ELI5
Before this PR, a native Claude or Codex chat always used the global Agent Permissions setting (Yolo or Manual), and there was no way to see or change it from the chat. Now every Claude and Codex chat has a button right after "+" in the composer that shows its mode and lets you change it: Ask for approval, Accept edits (Claude only), Approve for me, or Full access. New chats start from a default you set in Settings → Chat. A change applies before your next message, never in the middle of a reply.
What Changed
Before: Claude and Codex chats always ran with the global Agent Permissions setting. A chat couldn't run differently from it, and nothing in the chat showed what the agent was allowed to do.
After:
Mechanism:
--permission-mode default|acceptEdits|auto, or--dangerously-skip-permissionsfor Full access). Codex gets its approval policy, sandbox mode and reviewer onthread/start/thread/resume.Why
This replaces an earlier version of this PR (about 4,500 product lines) that switched Claude's mode live and needed its own ordering counter, a separate empty-chat setting, and per-turn Codex overrides. The common pattern is simpler: a mode change restarts the agent and resumes the conversation. This version reuses Orca's existing option storage and its existing "replace the agent before the next message" step, at about 1,350 product lines.
Differences from the common pattern:
on-request+workspace-write, main's existing Manual posture.Linked Issue
STA-8261. Merge after #26684: Codex's per-thread reviewer setting needs Codex 0.136.0, which #26684 requires.
Visual Proof
Captured in a hidden Electron app with stand-in Claude and Codex CLIs, which record the exact launch flags and protocol frames.
Testing
Live (hidden Electron, stand-in Claude/Codex logging argv and frames, timestamps checked against the stand-in logs):
--permission-mode acceptEdits.--dangerously-skip-permissionsresuming the same session. No notice, and across 38 samples the button never flashed back.thread/resumehad the same thread withon-request,workspace-writeandapprovalsReviewer: auto_review.turn/startcarried no reviewer.thread/resumecarried a sandbox mode, never asandboxPolicyobject.--permission-mode auto, one process for two messages;--permission-mode default.Automated:
After the final merge with main: typechecks, mobile typecheck and ratchet, changed-code quality, localization and max-lines all pass locally.
Not verified: the phone and the web app live (unit tests only), Windows, Linux, SSH.
Agent skill upstream boundary
Notes
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)