Orca CLI
diff --git a/src/renderer/src/components/settings/general-search.ts b/src/renderer/src/components/settings/general-search.ts
index 6a5ffbcb32c..332dbd68c15 100644
--- a/src/renderer/src/components/settings/general-search.ts
+++ b/src/renderer/src/components/settings/general-search.ts
@@ -80,9 +80,11 @@ export const GENERAL_NAVIGATION_SEARCH_ENTRIES: SettingsSearchEntry[] = [
export const GENERAL_CLI_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
- title: 'Shell command',
+ title: 'Orca CLI',
description: 'Register or remove the orca shell command.',
- keywords: ['cli', 'path', 'terminal', 'command']
+ keywords: ['cli', 'path', 'terminal', 'command', 'shell command'],
+ cmdJKeywords: ['cli', 'path', 'command', 'shell command'],
+ targetSectionId: 'cli'
},
{
title: 'Agent skill',
diff --git a/src/renderer/src/components/settings/settings-search.ts b/src/renderer/src/components/settings/settings-search.ts
index c72750bf546..59abab8a2b2 100644
--- a/src/renderer/src/components/settings/settings-search.ts
+++ b/src/renderer/src/components/settings/settings-search.ts
@@ -2,6 +2,8 @@ export type SettingsSearchEntry = {
title: string
description?: string
keywords?: string[]
+ cmdJKeywords?: string[]
+ targetSectionId?: string
}
export function normalizeSettingsSearchQuery(query: string): string {
diff --git a/src/renderer/src/lib/agent-feature-install-commands.ts b/src/renderer/src/lib/agent-feature-install-commands.ts
index 6607c22cd7d..030996c80eb 100644
--- a/src/renderer/src/lib/agent-feature-install-commands.ts
+++ b/src/renderer/src/lib/agent-feature-install-commands.ts
@@ -3,6 +3,7 @@ export {
COMPUTER_USE_SKILL_INSTALL_COMMAND,
COMPUTER_USE_SKILL_NAME,
ORCA_CLI_SKILL_INSTALL_COMMAND,
+ ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND,
ORCA_CLI_SKILL_NAME,
ORCHESTRATION_SKILL_NAME
} from '../../../shared/agent-feature-install-commands'
diff --git a/src/shared/agent-feature-install-commands.ts b/src/shared/agent-feature-install-commands.ts
index 0b9a4ed5826..fe9d227b3af 100644
--- a/src/shared/agent-feature-install-commands.ts
+++ b/src/shared/agent-feature-install-commands.ts
@@ -22,3 +22,8 @@ export const COMPUTER_USE_SKILL_INSTALL_COMMAND = buildAgentFeatureSkillInstallC
export const ORCHESTRATION_SKILL_INSTALL_COMMAND = buildAgentFeatureSkillInstallCommand([
ORCHESTRATION_SKILL_NAME
])
+
+export const ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND = buildAgentFeatureSkillInstallCommand([
+ ORCA_CLI_SKILL_NAME,
+ ORCHESTRATION_SKILL_NAME
+])
diff --git a/src/shared/feature-tips.test.ts b/src/shared/feature-tips.test.ts
index 56365ab72fd..f4737720676 100644
--- a/src/shared/feature-tips.test.ts
+++ b/src/shared/feature-tips.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
+ FEATURE_TIPS,
getCompletedFeatureTipIds,
getOrderedUnseenFeatureTips,
normalizeFeatureTipIds,
@@ -10,12 +11,12 @@ describe('feature tips', () => {
it('orders new unseen tips before older unseen tips', () => {
const tips = getOrderedUnseenFeatureTips({ seenTipIds: new Set() })
- expect(tips.map((tip) => tip.id)).toEqual(['voice-dictation'])
+ expect(tips.map((tip) => tip.id)).toEqual(['orca-cli', 'voice-dictation'])
})
it('skips tips the user has already seen', () => {
const tips = getOrderedUnseenFeatureTips({
- seenTipIds: new Set(['voice-dictation'])
+ seenTipIds: new Set(['voice-dictation', 'orca-cli'])
})
expect(tips.map((tip) => tip.id)).toEqual([])
@@ -24,7 +25,22 @@ describe('feature tips', () => {
it('skips tips for features the user has already completed', () => {
const tips = getOrderedUnseenFeatureTips({
seenTipIds: new Set(),
- completedTipIds: getCompletedFeatureTipIds({ voiceDictationEnabled: true })
+ completedTipIds: getCompletedFeatureTipIds({
+ cliInstalled: true,
+ voiceDictationEnabled: true
+ })
+ })
+
+ expect(tips.map((tip) => tip.id)).toEqual([])
+ })
+
+ it('skips the CLI tip when the CLI is already installed', () => {
+ const tips = getOrderedUnseenFeatureTips({
+ seenTipIds: new Set(['voice-dictation']),
+ completedTipIds: getCompletedFeatureTipIds({
+ cliInstalled: true,
+ voiceDictationEnabled: false
+ })
})
expect(tips.map((tip) => tip.id)).toEqual([])
@@ -34,6 +50,7 @@ describe('feature tips', () => {
const tips = getOrderedUnseenFeatureTips({
seenTipIds: new Set(),
completedTipIds: getCompletedFeatureTipIds({
+ cliInstalled: false,
voiceDictationEnabled: false,
featureInteractions: {
'voice-dictation': { firstInteractedAt: 100, interactionCount: 1 }
@@ -41,12 +58,31 @@ describe('feature tips', () => {
})
})
- expect(tips.map((tip) => tip.id)).toEqual([])
+ expect(tips.map((tip) => tip.id)).toEqual(['orca-cli'])
})
it('normalizes persisted tip ids', () => {
- expect(normalizeFeatureTipIds(['feature-tour', 'bogus', 'voice-dictation'])).toEqual([
- 'voice-dictation'
- ])
+ expect(
+ normalizeFeatureTipIds(['feature-tour', 'orca-cli', 'bogus', 'voice-dictation'])
+ ).toEqual(['orca-cli', 'voice-dictation'])
+ })
+
+ it('describes the CLI tip as an install action with concrete workflows', () => {
+ const cliTip = FEATURE_TIPS.find((tip) => tip.id === 'orca-cli')
+
+ expect(cliTip).toMatchObject({
+ action: 'setup-cli',
+ title: 'Let agents drive Orca with the Orca CLI',
+ ctaLabel: 'Install CLI & Skills'
+ })
+ expect(cliTip?.description).toContain('coordinate child workspaces')
+ expect(cliTip?.description).toContain('communicate between workspaces')
+ })
+
+ it('does not label the voice dictation tip as new', () => {
+ const voiceTip = FEATURE_TIPS.find((tip) => tip.id === 'voice-dictation')
+
+ expect(voiceTip?.eyebrow).toBe('Tip')
+ expect(voiceTip?.priority).toBe('unseen')
})
})
diff --git a/src/shared/feature-tips.ts b/src/shared/feature-tips.ts
index a4b8bc3713b..7db691fe9d1 100644
--- a/src/shared/feature-tips.ts
+++ b/src/shared/feature-tips.ts
@@ -4,11 +4,11 @@ import {
type FeatureInteractionState
} from './feature-interactions'
-export type FeatureTipId = 'voice-dictation'
+export type FeatureTipId = 'voice-dictation' | 'orca-cli'
export type FeatureTipPriority = 'new' | 'unseen'
-export type FeatureTipAction = 'enable-voice'
+export type FeatureTipAction = 'enable-voice' | 'setup-cli'
export type FeatureTip = {
id: FeatureTipId
@@ -23,15 +23,26 @@ export type FeatureTip = {
}
export type CompletedFeatureTipState = {
+ cliInstalled: boolean
voiceDictationEnabled: boolean
featureInteractions?: FeatureInteractionState
}
export const FEATURE_TIPS = [
{
- id: 'voice-dictation',
+ id: 'orca-cli',
priority: 'new',
- eyebrow: 'New',
+ eyebrow: 'Tip',
+ title: 'Let agents drive Orca with the Orca CLI',
+ description: 'Enable agents to coordinate child workspaces and communicate between workspaces.',
+ action: 'setup-cli',
+ ctaLabel: 'Install CLI & Skills',
+ completedByFeatureInteractions: []
+ },
+ {
+ id: 'voice-dictation',
+ priority: 'unseen',
+ eyebrow: 'Tip',
title: 'Voice Dictation is here',
description:
'Speak into any focused pane and Orca will transcribe it. Press the dictation shortcut to start and stop.',
@@ -63,6 +74,9 @@ export function normalizeFeatureTipIds(value: unknown): FeatureTipId[] {
export function getCompletedFeatureTipIds(state: CompletedFeatureTipState): Set {
const completedIds = new Set()
+ if (state.cliInstalled) {
+ completedIds.add('orca-cli')
+ }
if (state.voiceDictationEnabled) {
completedIds.add('voice-dictation')
}
diff --git a/src/shared/telemetry-events.test.ts b/src/shared/telemetry-events.test.ts
index 29e91721643..90924dd5033 100644
--- a/src/shared/telemetry-events.test.ts
+++ b/src/shared/telemetry-events.test.ts
@@ -265,6 +265,46 @@ describe('settings_changed schema', () => {
})
})
+describe('orca cli feature tip schemas', () => {
+ it('accepts the shown event for app-open exposure', () => {
+ const parsed = eventSchemas.orca_cli_feature_tip_shown.safeParse({
+ source: 'app_open'
+ })
+
+ expect(parsed.success).toBe(true)
+ })
+
+ it('accepts setup click and setup result events', () => {
+ expect(
+ eventSchemas.orca_cli_feature_tip_setup_clicked.safeParse({
+ source: 'app_open'
+ }).success
+ ).toBe(true)
+ expect(
+ eventSchemas.orca_cli_feature_tip_setup_result.safeParse({
+ source: 'app_open',
+ result: 'installed'
+ }).success
+ ).toBe(true)
+ })
+
+ it('rejects raw CLI details and unknown result values', () => {
+ expect(
+ eventSchemas.orca_cli_feature_tip_setup_result.safeParse({
+ source: 'app_open',
+ result: 'installed',
+ command_path: '/Users/alice/bin/orca'
+ }).success
+ ).toBe(false)
+ expect(
+ eventSchemas.orca_cli_feature_tip_setup_result.safeParse({
+ source: 'app_open',
+ result: 'installed_after_retry'
+ }).success
+ ).toBe(false)
+ })
+})
+
describe('commonPropsSchema', () => {
it('round-trips a realistic payload', () => {
const parsed = commonPropsSchema.safeParse({
diff --git a/src/shared/telemetry-events.ts b/src/shared/telemetry-events.ts
index fb62716ceeb..fe00d07f9c8 100644
--- a/src/shared/telemetry-events.ts
+++ b/src/shared/telemetry-events.ts
@@ -323,6 +323,27 @@ const settingsChangedSchema = z
const telemetryOptedInSchema = z.object({ via: optInViaSchema }).strict()
const telemetryOptedOutSchema = z.object({ via: optInViaSchema }).strict()
+const orcaCliFeatureTipSourceSchema = z.enum(['app_open', 'manual'])
+const orcaCliFeatureTipShownSchema = z
+ .object({
+ source: orcaCliFeatureTipSourceSchema,
+ nth_repo_added: nthRepoAddedSchema
+ })
+ .strict()
+const orcaCliFeatureTipSetupClickedSchema = z
+ .object({
+ source: orcaCliFeatureTipSourceSchema,
+ nth_repo_added: nthRepoAddedSchema
+ })
+ .strict()
+const orcaCliFeatureTipSetupResultSchema = z
+ .object({
+ source: orcaCliFeatureTipSourceSchema,
+ result: z.enum(['installed', 'needs_attention', 'dev_preview', 'failed']),
+ nth_repo_added: nthRepoAddedSchema
+ })
+ .strict()
+
const featureWallOpenedSchema = z
.object({
source: featureWallOpenSourceSchema
@@ -1046,6 +1067,10 @@ export const eventSchemas = {
telemetry_opted_in: telemetryOptedInSchema,
telemetry_opted_out: telemetryOptedOutSchema,
+ orca_cli_feature_tip_shown: orcaCliFeatureTipShownSchema,
+ orca_cli_feature_tip_setup_clicked: orcaCliFeatureTipSetupClickedSchema,
+ orca_cli_feature_tip_setup_result: orcaCliFeatureTipSetupResultSchema,
+
feature_wall_opened: featureWallOpenedSchema,
feature_wall_closed: featureWallClosedSchema,
feature_wall_tile_focused: featureWallTileFocusedSchema,
@@ -1130,6 +1155,9 @@ type _CohortExtendedRoster =
| 'agent_started'
| 'agent_prompt_sent'
| 'agent_error'
+ | 'orca_cli_feature_tip_shown'
+ | 'orca_cli_feature_tip_setup_clicked'
+ | 'orca_cli_feature_tip_setup_result'
// Why: `z.object({}).strict()` infers a string index signature, which would
// make every key appear present. Ignore index-signature-only keys here so
// strict empty event payloads do not get pulled into keyed telemetry rosters.
diff --git a/tests/e2e/helpers/e2e-completed-onboarding-profile.ts b/tests/e2e/helpers/e2e-completed-onboarding-profile.ts
index 8127616b7fb..74bb5494a16 100644
--- a/tests/e2e/helpers/e2e-completed-onboarding-profile.ts
+++ b/tests/e2e/helpers/e2e-completed-onboarding-profile.ts
@@ -1,6 +1,6 @@
import { ONBOARDING_FINAL_STEP } from '../../../src/shared/constants'
-const SEEN_FIRST_RUN_FEATURE_TIP_IDS = ['voice-dictation'] as const
+const SEEN_FIRST_RUN_FEATURE_TIP_IDS = ['voice-dictation', 'orca-cli'] as const
export function getE2ECompletedOnboardingProfile() {
return {
From a1496d54d40cf6d06f3e5f7f2ebe44e13487cde5 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Fri, 29 May 2026 16:25:10 -0700
Subject: [PATCH 022/224] Document upstream React effect baseline (#3369)
---
docs/reference/react-performance-audit.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/reference/react-performance-audit.md b/docs/reference/react-performance-audit.md
index 12bfe36f779..41a6d1cb27a 100644
--- a/docs/reference/react-performance-audit.md
+++ b/docs/reference/react-performance-audit.md
@@ -47,13 +47,13 @@ Initial inventory:
## Coverage Ledger
-Current count after low-risk PRs #3038, #3041, #3042, #3044, #3051, #3052, #3053, #3054, #3055, #3056, #3058, #3059, #3060, #3062, #3063, #3064, #3065, #3066, #3067, #3068, #3069, #3083, #3087, #3091, #3100, #3104, #3122, #3160, #3164, #3166, #3223, #3244, #3271, #3273, #3294, #3296, #3298, #3300, and #3330, plus upstream tab-entry changes #3011 and #3026: 911 Effect hook call sites.
+Current count after low-risk PRs #3038, #3041, #3042, #3044, #3051, #3052, #3053, #3054, #3055, #3056, #3058, #3059, #3060, #3062, #3063, #3064, #3065, #3066, #3067, #3068, #3069, #3083, #3087, #3091, #3100, #3104, #3122, #3160, #3164, #3166, #3223, #3244, #3271, #3273, #3294, #3296, #3298, #3300, and #3330, plus upstream tab-entry changes #3011 and #3026 and upstream Orca CLI feature-tip changes #3279: 914 Effect hook call sites.
-Open medium-risk PRs #3070, #3073, #3077, #3089, #3093, #3102, #3108, #3110, #3112, #3118, #3124, #3126, #3128, #3130, #3132, #3134, #3142, #3144, #3146, #3148, #3150, #3152, #3154, #3170, #3176, #3184, #3186, #3188, #3192, #3195, #3198, #3200, #3202, #3206, #3218, #3220, #3226, #3228, #3234, #3240, #3242, #3247, #3257, #3269, #3275, #3277, #3283, #3289, #3292, #3303, #3311, #3314, #3316, #3318, #3322, #3328, #3333, #3338, #3340, #3344, #3347, #3349, #3354, #3356, #3361, and #3365 each project to 910 Effect hook call sites on the current merged baseline; open medium-risk PRs #3097, #3106, #3116, #3120, #3136, #3138, #3180, #3210, #3214, #3238, #3255, #3261, #3281, #3285, and #3287 each project to 909; open medium-risk PRs #3114, #3168, and #3259 each project to 908; open medium-risk PRs #3095, #3140, and #3230 each project to 907; open medium-risk PR #3326 projects to 902; open medium-risk PR #3079 projects to 901; open high-risk PRs #3157, #3212, #3253, #3351, and #3367 each project to 910; open high-risk PR #3267 projects to 909; open high-risk PR #3075 projects to 907; open high-risk PR #3250 projects to 906; and open high-risk PR #3081 projects to 903. These are not counted in the merged baseline until reviewed and merged.
+Open medium-risk PRs #3070, #3073, #3077, #3089, #3093, #3102, #3108, #3110, #3112, #3118, #3124, #3126, #3128, #3130, #3132, #3134, #3142, #3144, #3146, #3148, #3150, #3152, #3154, #3170, #3176, #3184, #3186, #3188, #3192, #3195, #3198, #3200, #3202, #3206, #3218, #3220, #3226, #3228, #3234, #3240, #3242, #3247, #3257, #3269, #3275, #3277, #3283, #3289, #3292, #3303, #3311, #3314, #3316, #3318, #3322, #3328, #3333, #3338, #3340, #3344, #3347, #3349, #3354, #3356, #3361, and #3365 each project to 913 Effect hook call sites on the current merged baseline; open medium-risk PRs #3097, #3106, #3116, #3120, #3136, #3138, #3180, #3210, #3214, #3238, #3255, #3261, #3281, #3285, and #3287 each project to 912; open medium-risk PRs #3114, #3168, and #3259 each project to 911; open medium-risk PRs #3095, #3140, and #3230 each project to 910; open medium-risk PR #3326 projects to 905; open medium-risk PR #3079 projects to 904; open high-risk PRs #3157, #3212, #3253, #3351, and #3367 each project to 913; open high-risk PR #3267 projects to 912; open high-risk PR #3075 projects to 910; open high-risk PR #3250 projects to 909; and open high-risk PR #3081 projects to 906. These are not counted in the merged baseline until reviewed and merged.
| Area | Files / signal | Scan status | Notes |
| ------------------------------ | -------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
-| Renderer app shell | `src/renderer/src/App.tsx`, root components | Inventory complete, manual review in progress | Confirmation dialog active request mirror covered by #3091; onboarding settings detour reset covered by #3122; quick workspace agent selection repair covered by #3148; Cmd+J selection repair covered by #3164; smart workspace command highlight covered by #3192; App lazy-modal mount bookkeeping covered by #3220; shared reduced-motion media-query hook covered by #3287; tab-entry selected action reset covered by #3311. Continue checking global listeners, beforeunload, media-query, and active-tab repair. |
+| Renderer app shell | `src/renderer/src/App.tsx`, root components | Inventory complete, manual review in progress | Confirmation dialog active request mirror covered by #3091; onboarding settings detour reset covered by #3122; quick workspace agent selection repair covered by #3148; Cmd+J selection repair covered by #3164; smart workspace command highlight covered by #3192; App lazy-modal mount bookkeeping covered by #3220; shared reduced-motion media-query hook covered by #3287; tab-entry selected action reset covered by #3311; upstream #3279 added feature-tip startup gating to inspect. Continue checking global listeners, beforeunload, media-query, and active-tab repair. |
| Terminal / PTY | `components/Terminal.tsx`, `components/terminal-pane/**`, `components/terminal/**` | Inventory complete, manual review pending | Mobile driver overlay collapse repair covered by #3170; floating workspace first-open bounds normalization covered by #3269; unused legacy terminal shell/shortcut/hook files deleted by #3271; terminal tab rename focus covered by #3314 without PTY protocol changes; terminal search focus covered by #3344; Activity terminal portal subscription covered by #3367. High-risk area: xterm lifecycle, scrollback, remote/mobile parity, focus, WebGL, resize. |
| Renderer mobile page | `components/mobile/**` | Inventory complete, manual review in progress | Mobile pairing page stage repair covered by #3180. Continue checking QR generation, mobile-page IPC, and paired-device polling. |
| Browser pane | `components/browser-pane/**` | Inventory complete, manual review in progress | Highest Effect density: 62 sites in `BrowserPane.tsx`. Browser ref mirrors covered by #3081; BrowserFind debounced-query state covered by #3275. Continue with driver sync, address bar derived state, remaining find state, webview lifetime. |
@@ -64,7 +64,7 @@ Open medium-risk PRs #3070, #3073, #3077, #3089, #3093, #3102, #3108, #3110, #31
| Settings | `components/settings/**` | Inventory complete, manual review in progress | Repository hook local editor visibility covered by #3093; lazy section mounted-state bookkeeping covered by #3128; browser home/Kagi draft sync covered by #3136; general auto-save/Open In draft sync covered by #3138; Source Control AI instruction drafts covered by #3140; notification volume draft sync covered by #3142; repo Source Control AI override draft sync covered by #3144; Quick Commands add intent covered by #3146; Mobile QR polling count capture covered by #3152; integration preflight status derivation covered by #3154; SSH reset-relay dialog cleanup covered by #3157; diagnostics bundle cleanup ref mirror covered by #3176; font autocomplete option scroll covered by #3349; SSH passphrase prompt focus covered by #3351. Continue with draft mirrors while keeping SSH and cross-platform settings behavior intact. |
| Automations | `components/automations/**` | Inventory complete, manual review in progress | Combobox open-focus Effects covered by #3100; run-history selection repair covered by #3104; external run-table selection repair covered by #3102; detail-page selection resets covered by #3106; create-from branch search reset covered by #3132. Continue with draft defaulting, polling, and completion detection. |
| Issue, PR, task pages | `TaskPage.tsx`, `PullRequestPage.tsx`, `GitHubItemDialog.tsx`, `GitLabItemDialog.tsx`, Linear components | Inventory complete, manual review in progress | Linear estimate input sync covered by #3108; Linear sub-issue list sync covered by #3110; GitHub project date draft sync covered by #3112; GitHub reviewer-picker active index covered by #3114; GitHub project column state covered by #3116; GitHub Project visible-table cache covered by #3186; GitHub Project dialog state covered by #3214; GitLab comment draft reset covered by #3124; Linear title/description draft sync covered by #3130; GitHub assignee dirty-flag reset covered by #3255; GitHub reviewer-request local sync covered by #3259; GitHub link-copy feedback timers covered by #3285; GitLab task filter repair covered by #3292. Continue large-file scan with separate GitHub, GitLab, Linear, and generic review behavior. |
-| Onboarding / feature wall | `components/onboarding/**`, `components/feature-wall/**` | Inventory complete, manual review in progress | Select portal root Effects covered by #3087; tour visited-marker Effects covered by #3095; persisted completion close reset covered by #3188; onboarding settings hydration covered by #3198; unused orchestration notifications page deleted by #3273; feature-wall skill installed-state notification Effects covered by #3281; session-depth open publish Effects covered by #3283; shared reduced-motion media-query hook covered by #3287. Continue with demo timers and telemetry while avoiding telemetry semantic changes. |
+| Onboarding / feature wall | `components/onboarding/**`, `components/feature-wall/**` | Inventory complete, manual review in progress | Select portal root Effects covered by #3087; tour visited-marker Effects covered by #3095; persisted completion close reset covered by #3188; onboarding settings hydration covered by #3198; unused orchestration notifications page deleted by #3273; feature-wall skill installed-state notification Effects covered by #3281; session-depth open publish Effects covered by #3283; shared reduced-motion media-query hook covered by #3287; upstream #3279 added feature-tip modal and orchestration visual Effects to inspect. Continue with demo timers and telemetry while avoiding telemetry semantic changes. |
| Status, dashboard, activity | `components/status-bar/**`, `components/dashboard/**`, `components/activity/**`, `components/pet/**` | Inventory complete, manual review in progress | Status-bar account menu close reset covered by #3051; pet overlay size clamp covered by #3118; Activity selected-thread repair covered by #3126; Workspace Space row-state repairs covered by #3168; Resource Manager Space scan-ready latch covered by #3200; pet reduced-motion subscription covered by #3287; status-bar account usage fetch covered by #3340; Space page open interaction covered by #3361. Check interval sharing, retained agent state, activity terminal portals. |
| Mobile app routes | `mobile/app/**` | Inventory complete, manual review in progress | Pair-confirm route-code parsing covered by #3240; host home new-worktree route action covered by #3242; Accounts unused client ref covered by #3244; mobile home shared-client ref mirror covered by #3247; session route terminal/tab ref mirrors covered by #3250; tasks route client ref mirror covered by #3253; tasks route workspace agent selection covered by #3261. Continue with large `tasks.tsx` state/polling Effects, session route cleanup Effects, home route ref mirrors, and remote-client parity. |
| Mobile shared source | `mobile/src/**` | Inventory complete, manual review in progress | TextInputModal and CustomKeyModal open-state resets covered by #3223; BottomDrawer mount bookkeeping covered by #3226; MobileBrowserPane address sync covered by #3228; MobileBrowserPane ref mirrors covered by #3230; useMobileDictation ref mirrors covered by #3234; NewWorktreeModal agent selection covered by #3238. Continue with transport client context plus NewWorktreeModal open/reset, SSH, agent-detection, and setup-hook fetch Effects. |
From 425dee5085078f9730d8d396effa04aff77123f0 Mon Sep 17 00:00:00 2001
From: "buf0-bot[bot]" <252831055+buf0-bot[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 16:29:40 -0700
Subject: [PATCH 023/224] fix: pr-bug-scan validated finding from #2793 (#3024)
Co-authored-by: orca-bug-scan-bot
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
---
.../components/right-sidebar/FileExplorer.tsx | 11 +++++++++
.../right-sidebar/file-explorer-reset.test.ts | 24 +++++++++++++++++++
.../right-sidebar/file-explorer-reset.ts | 6 +++++
3 files changed, 41 insertions(+)
create mode 100644 src/renderer/src/components/right-sidebar/file-explorer-reset.test.ts
create mode 100644 src/renderer/src/components/right-sidebar/file-explorer-reset.ts
diff --git a/src/renderer/src/components/right-sidebar/FileExplorer.tsx b/src/renderer/src/components/right-sidebar/FileExplorer.tsx
index 303ffef219a..6809f3cc642 100644
--- a/src/renderer/src/components/right-sidebar/FileExplorer.tsx
+++ b/src/renderer/src/components/right-sidebar/FileExplorer.tsx
@@ -8,6 +8,7 @@ import { folderRelativePathToIncludeGlob } from './file-search-include-pattern'
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/lib/utils'
import { isGitRepoKind } from '../../../../shared/repo-kind'
+import { shouldResetFileExplorerForVisibleWorktree } from './file-explorer-reset'
import { FileExplorerBackgroundMenu } from './FileExplorerBackgroundMenu'
import { FileExplorerToolbar } from './FileExplorerToolbar'
import { FileExplorerTreeStatus } from './FileExplorerTreeStatus'
@@ -157,12 +158,22 @@ function FileExplorerInner(): React.JSX.Element {
scrollRef
})
+ const lastResetWorktreePathRef = useRef(null)
useEffect(() => {
if (!visibleWorktreePath) {
return
}
// Why: the sidebar remains mounted while closed to preserve caches, but
// loading the hidden tree would probe every clicked workspace on macOS.
+ if (
+ !shouldResetFileExplorerForVisibleWorktree(
+ lastResetWorktreePathRef.current,
+ visibleWorktreePath
+ )
+ ) {
+ return
+ }
+ lastResetWorktreePathRef.current = visibleWorktreePath
resetSelection()
resetAndLoad()
clearFileExplorerUndoHistory()
diff --git a/src/renderer/src/components/right-sidebar/file-explorer-reset.test.ts b/src/renderer/src/components/right-sidebar/file-explorer-reset.test.ts
new file mode 100644
index 00000000000..66a341c4c9d
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/file-explorer-reset.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest'
+import { shouldResetFileExplorerForVisibleWorktree } from './file-explorer-reset'
+
+describe('shouldResetFileExplorerForVisibleWorktree', () => {
+ it('preserves explorer state across hide and reopen of the same worktree', () => {
+ let lastResetWorktreePath: string | null = null
+ const shouldReset = (visibleWorktreePath: string | null): boolean => {
+ if (shouldResetFileExplorerForVisibleWorktree(lastResetWorktreePath, visibleWorktreePath)) {
+ lastResetWorktreePath = visibleWorktreePath
+ return true
+ }
+ return false
+ }
+
+ expect(shouldReset(null)).toBe(false)
+ expect(shouldReset('/repo')).toBe(true)
+ expect(shouldReset(null)).toBe(false)
+ expect(shouldReset('/repo')).toBe(false)
+ })
+
+ it('resets when the visible worktree path changes', () => {
+ expect(shouldResetFileExplorerForVisibleWorktree('/repo', '/repo-next')).toBe(true)
+ })
+})
diff --git a/src/renderer/src/components/right-sidebar/file-explorer-reset.ts b/src/renderer/src/components/right-sidebar/file-explorer-reset.ts
new file mode 100644
index 00000000000..2473fe9a874
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/file-explorer-reset.ts
@@ -0,0 +1,6 @@
+export function shouldResetFileExplorerForVisibleWorktree(
+ lastResetWorktreePath: string | null,
+ visibleWorktreePath: string | null
+): visibleWorktreePath is string {
+ return visibleWorktreePath !== null && lastResetWorktreePath !== visibleWorktreePath
+}
From a99c376f3bd0b219d992786bc3c12743b2a0a037 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Fri, 29 May 2026 16:32:21 -0700
Subject: [PATCH 024/224] Document upstream feature tip effect audit (#3372)
---
docs/reference/react-performance-audit.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/reference/react-performance-audit.md b/docs/reference/react-performance-audit.md
index 41a6d1cb27a..8a2529da1e1 100644
--- a/docs/reference/react-performance-audit.md
+++ b/docs/reference/react-performance-audit.md
@@ -53,7 +53,7 @@ Open medium-risk PRs #3070, #3073, #3077, #3089, #3093, #3102, #3108, #3110, #31
| Area | Files / signal | Scan status | Notes |
| ------------------------------ | -------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
-| Renderer app shell | `src/renderer/src/App.tsx`, root components | Inventory complete, manual review in progress | Confirmation dialog active request mirror covered by #3091; onboarding settings detour reset covered by #3122; quick workspace agent selection repair covered by #3148; Cmd+J selection repair covered by #3164; smart workspace command highlight covered by #3192; App lazy-modal mount bookkeeping covered by #3220; shared reduced-motion media-query hook covered by #3287; tab-entry selected action reset covered by #3311; upstream #3279 added feature-tip startup gating to inspect. Continue checking global listeners, beforeunload, media-query, and active-tab repair. |
+| Renderer app shell | `src/renderer/src/App.tsx`, root components | Inventory complete, manual review in progress | Confirmation dialog active request mirror covered by #3091; onboarding settings detour reset covered by #3122; quick workspace agent selection repair covered by #3148; Cmd+J selection repair covered by #3164; smart workspace command highlight covered by #3192; App lazy-modal mount bookkeeping covered by #3220; shared reduced-motion media-query hook covered by #3287; tab-entry selected action reset covered by #3311; upstream #3279 feature-tip startup gating inspected: app-open prompt/seen-marking and shown telemetry are display-state/analytics sync, so no change. Continue checking global listeners, beforeunload, media-query, and active-tab repair. |
| Terminal / PTY | `components/Terminal.tsx`, `components/terminal-pane/**`, `components/terminal/**` | Inventory complete, manual review pending | Mobile driver overlay collapse repair covered by #3170; floating workspace first-open bounds normalization covered by #3269; unused legacy terminal shell/shortcut/hook files deleted by #3271; terminal tab rename focus covered by #3314 without PTY protocol changes; terminal search focus covered by #3344; Activity terminal portal subscription covered by #3367. High-risk area: xterm lifecycle, scrollback, remote/mobile parity, focus, WebGL, resize. |
| Renderer mobile page | `components/mobile/**` | Inventory complete, manual review in progress | Mobile pairing page stage repair covered by #3180. Continue checking QR generation, mobile-page IPC, and paired-device polling. |
| Browser pane | `components/browser-pane/**` | Inventory complete, manual review in progress | Highest Effect density: 62 sites in `BrowserPane.tsx`. Browser ref mirrors covered by #3081; BrowserFind debounced-query state covered by #3275. Continue with driver sync, address bar derived state, remaining find state, webview lifetime. |
@@ -64,7 +64,7 @@ Open medium-risk PRs #3070, #3073, #3077, #3089, #3093, #3102, #3108, #3110, #31
| Settings | `components/settings/**` | Inventory complete, manual review in progress | Repository hook local editor visibility covered by #3093; lazy section mounted-state bookkeeping covered by #3128; browser home/Kagi draft sync covered by #3136; general auto-save/Open In draft sync covered by #3138; Source Control AI instruction drafts covered by #3140; notification volume draft sync covered by #3142; repo Source Control AI override draft sync covered by #3144; Quick Commands add intent covered by #3146; Mobile QR polling count capture covered by #3152; integration preflight status derivation covered by #3154; SSH reset-relay dialog cleanup covered by #3157; diagnostics bundle cleanup ref mirror covered by #3176; font autocomplete option scroll covered by #3349; SSH passphrase prompt focus covered by #3351. Continue with draft mirrors while keeping SSH and cross-platform settings behavior intact. |
| Automations | `components/automations/**` | Inventory complete, manual review in progress | Combobox open-focus Effects covered by #3100; run-history selection repair covered by #3104; external run-table selection repair covered by #3102; detail-page selection resets covered by #3106; create-from branch search reset covered by #3132. Continue with draft defaulting, polling, and completion detection. |
| Issue, PR, task pages | `TaskPage.tsx`, `PullRequestPage.tsx`, `GitHubItemDialog.tsx`, `GitLabItemDialog.tsx`, Linear components | Inventory complete, manual review in progress | Linear estimate input sync covered by #3108; Linear sub-issue list sync covered by #3110; GitHub project date draft sync covered by #3112; GitHub reviewer-picker active index covered by #3114; GitHub project column state covered by #3116; GitHub Project visible-table cache covered by #3186; GitHub Project dialog state covered by #3214; GitLab comment draft reset covered by #3124; Linear title/description draft sync covered by #3130; GitHub assignee dirty-flag reset covered by #3255; GitHub reviewer-request local sync covered by #3259; GitHub link-copy feedback timers covered by #3285; GitLab task filter repair covered by #3292. Continue large-file scan with separate GitHub, GitLab, Linear, and generic review behavior. |
-| Onboarding / feature wall | `components/onboarding/**`, `components/feature-wall/**` | Inventory complete, manual review in progress | Select portal root Effects covered by #3087; tour visited-marker Effects covered by #3095; persisted completion close reset covered by #3188; onboarding settings hydration covered by #3198; unused orchestration notifications page deleted by #3273; feature-wall skill installed-state notification Effects covered by #3281; session-depth open publish Effects covered by #3283; shared reduced-motion media-query hook covered by #3287; upstream #3279 added feature-tip modal and orchestration visual Effects to inspect. Continue with demo timers and telemetry while avoiding telemetry semantic changes. |
+| Onboarding / feature wall | `components/onboarding/**`, `components/feature-wall/**` | Inventory complete, manual review in progress | Select portal root Effects covered by #3087; tour visited-marker Effects covered by #3095; persisted completion close reset covered by #3188; onboarding settings hydration covered by #3198; unused orchestration notifications page deleted by #3273; feature-wall skill installed-state notification Effects covered by #3281; session-depth open publish Effects covered by #3283; shared reduced-motion media-query hook covered by #3287; upstream #3279 feature-tip modal and orchestration visual Effects inspected: CLI command reveal timers and DOM arrow/demo animation stay classified as visual external sync. Continue with demo timers and telemetry while avoiding telemetry semantic changes. |
| Status, dashboard, activity | `components/status-bar/**`, `components/dashboard/**`, `components/activity/**`, `components/pet/**` | Inventory complete, manual review in progress | Status-bar account menu close reset covered by #3051; pet overlay size clamp covered by #3118; Activity selected-thread repair covered by #3126; Workspace Space row-state repairs covered by #3168; Resource Manager Space scan-ready latch covered by #3200; pet reduced-motion subscription covered by #3287; status-bar account usage fetch covered by #3340; Space page open interaction covered by #3361. Check interval sharing, retained agent state, activity terminal portals. |
| Mobile app routes | `mobile/app/**` | Inventory complete, manual review in progress | Pair-confirm route-code parsing covered by #3240; host home new-worktree route action covered by #3242; Accounts unused client ref covered by #3244; mobile home shared-client ref mirror covered by #3247; session route terminal/tab ref mirrors covered by #3250; tasks route client ref mirror covered by #3253; tasks route workspace agent selection covered by #3261. Continue with large `tasks.tsx` state/polling Effects, session route cleanup Effects, home route ref mirrors, and remote-client parity. |
| Mobile shared source | `mobile/src/**` | Inventory complete, manual review in progress | TextInputModal and CustomKeyModal open-state resets covered by #3223; BottomDrawer mount bookkeeping covered by #3226; MobileBrowserPane address sync covered by #3228; MobileBrowserPane ref mirrors covered by #3230; useMobileDictation ref mirrors covered by #3234; NewWorktreeModal agent selection covered by #3238. Continue with transport client context plus NewWorktreeModal open/reset, SSH, agent-detection, and setup-hook fetch Effects. |
From 0cc3bd77bca5cb7ee5af6724d8b161dad0ae1741 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Fri, 29 May 2026 16:34:08 -0700
Subject: [PATCH 025/224] docs: update final memory leak comb after rebase
(#3373)
---
.../reference/memory-leak-audit-final-comb.md | 28 +++++++++++++++----
1 file changed, 23 insertions(+), 5 deletions(-)
diff --git a/docs/reference/memory-leak-audit-final-comb.md b/docs/reference/memory-leak-audit-final-comb.md
index 0608705f10a..6aa1dcc61a1 100644
--- a/docs/reference/memory-leak-audit-final-comb.md
+++ b/docs/reference/memory-leak-audit-final-comb.md
@@ -2,7 +2,7 @@
Started: 2026-05-29 PDT
-Final tracker branch: `nwparker/mem-leak-final-comb-tracker`, based on `origin/main` at `869661cbba` after the final-comb fix PRs merged.
+Current tracker branch: `nwparker/mem-leak-final-comb-current`, based on `origin/main` at `a99c376f3b` after rebasing over later mainline changes.
Objective: run one more complete pass across the current Orca tree for missed listener, timer, observer, worker, socket, watcher, subscription, abort, and disposable leaks; open scoped PRs for confirmed stranglers; include risk level and merge low-risk PRs.
@@ -12,9 +12,9 @@ Tracked code files, from `git ls-files`:
| Bucket | Files | Status |
| ----------------- | ----: | ------- |
-| `src/renderer` | 1584 | Checked |
-| `src/main` | 856 | Checked |
-| `src/shared` | 245 | Checked |
+| `src/renderer` | 1592 | Checked |
+| `src/main` | 867 | Checked |
+| `src/shared` | 246 | Checked |
| `mobile/src` | 100 | Checked |
| `tests/e2e` | 72 | Checked |
| `src/relay` | 65 | Checked |
@@ -24,7 +24,7 @@ Tracked code files, from `git ls-files`:
| `other-code` | 5 | Checked |
| `src/preload` | 6 | Checked |
| `mobile/packages` | 5 | Checked |
-| Total | 3052 | Checked |
+| Total | 3072 | Checked |
## Scan Log
@@ -40,6 +40,17 @@ Tracked code files, from `git ls-files`:
- Abort controllers without `abort`: 1 test-only controller passed through the abortable API under test.
- Watchers without close/unwatch: no production misses; remaining heuristic hits are provider type/comment references.
- 2026-05-29: Re-ran React effect scan and spot-checked candidates. Remaining hits had cleanup returns (`clearTimeout`, `clearInterval`, `removeEventListener`, IPC unsubscribe) or were short one-shot UI focus/open delays.
+- 2026-05-29: Rebased the final comb over current `origin/main` at `a99c376f3b` after additional mainline merges.
+- 2026-05-29: Counted 3345 broad risk-pattern hits across `src`, `mobile`, `tests`, `config`, and `native` on the current tree.
+- 2026-05-29: Re-ran heuristic buckets on the current tree:
+ - `addEventListener` without same-file `removeEventListener`: 37 remaining, same reviewed categories as the previous final-comb pass.
+ - `setInterval` without `clearInterval`: 1 remaining, a test comment string.
+ - `setTimeout` without same-file `clearTimeout`: 235 remaining. New changed-file production hits were intentional app relaunch/restart delays before process exit and a zero-delay floating-terminal focus handoff. Other changed timer candidates had explicit cleanup.
+ - Observers without `disconnect`: 0.
+ - Workers without `terminate`: 0.
+ - Abort controllers without `abort`: 1 test-only controller passed through the abortable API under test.
+ - Watchers without close/unwatch: no production misses; remaining heuristic hits are provider type/comment references.
+- 2026-05-29: Focus-reviewed code changed since the previous tracker merge (`ca15aeb466..a99c376f3b`), including WSL runtime/account selection, daemon PTY checkpointing, main-window/rate-limit listeners, preload IPC subscriptions, feature-tip/settings/status-bar UI, feature-wall animation effects, FileExplorer reset effects, and renderer IPC subscriptions. No new confirmed leak needed a code PR.
## Findings
@@ -51,6 +62,8 @@ Tracked code files, from `git ls-files`:
No higher-risk findings remained after the final pass, so no second higher-risk mitigation pass was needed.
+Current rebase verification found no additional confirmed findings beyond the already merged low-risk PRs above.
+
## Validation
- `pnpm vitest run --config config/vitest.config.ts src/main/browser/agent-browser-bridge.test.ts`
@@ -59,3 +72,8 @@ No higher-risk findings remained after the final pass, so no second higher-risk
- `pnpm vitest run --config config/vitest.config.ts src/main/ipc/developer-permissions.test.ts`
- `pnpm exec oxlint src/main/browser/agent-browser-bridge.ts src/main/browser/agent-browser-bridge.test.ts src/main/ipc/notifications.ts src/main/ipc/notifications.test.ts src/main/computer/permissions.ts src/main/computer/permissions.test.ts src/main/ipc/developer-permissions.ts src/main/ipc/developer-permissions.test.ts`
- `pnpm run typecheck:node`
+- Current rebase pass:
+ - `git ls-files 'src/**/*.ts' 'src/**/*.tsx' 'mobile/**/*.ts' 'mobile/**/*.tsx' 'tests/**/*.ts' 'tests/**/*.tsx' 'config/**/*.js' 'config/**/*.mjs' 'native/**/*.ts'`
+ - broad `rg` risk-pattern count across `src`, `mobile`, `tests`, `config`, and `native`
+ - heuristic scans for unmatched listeners, intervals, timers, observers, workers, abort controllers, watchers, React effects, changed-file subscriptions, and changed-file timers
+ - manual changed-file review from `ca15aeb466` to `a99c376f3b`
From 431b336f1ba115354ada2e2357660aa1df88d3e1 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Fri, 29 May 2026 16:39:10 -0700
Subject: [PATCH 026/224] Document project column preference audit (#3375)
---
docs/reference/react-performance-audit.md | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/docs/reference/react-performance-audit.md b/docs/reference/react-performance-audit.md
index 8a2529da1e1..a1d75a18326 100644
--- a/docs/reference/react-performance-audit.md
+++ b/docs/reference/react-performance-audit.md
@@ -49,7 +49,7 @@ Initial inventory:
Current count after low-risk PRs #3038, #3041, #3042, #3044, #3051, #3052, #3053, #3054, #3055, #3056, #3058, #3059, #3060, #3062, #3063, #3064, #3065, #3066, #3067, #3068, #3069, #3083, #3087, #3091, #3100, #3104, #3122, #3160, #3164, #3166, #3223, #3244, #3271, #3273, #3294, #3296, #3298, #3300, and #3330, plus upstream tab-entry changes #3011 and #3026 and upstream Orca CLI feature-tip changes #3279: 914 Effect hook call sites.
-Open medium-risk PRs #3070, #3073, #3077, #3089, #3093, #3102, #3108, #3110, #3112, #3118, #3124, #3126, #3128, #3130, #3132, #3134, #3142, #3144, #3146, #3148, #3150, #3152, #3154, #3170, #3176, #3184, #3186, #3188, #3192, #3195, #3198, #3200, #3202, #3206, #3218, #3220, #3226, #3228, #3234, #3240, #3242, #3247, #3257, #3269, #3275, #3277, #3283, #3289, #3292, #3303, #3311, #3314, #3316, #3318, #3322, #3328, #3333, #3338, #3340, #3344, #3347, #3349, #3354, #3356, #3361, and #3365 each project to 913 Effect hook call sites on the current merged baseline; open medium-risk PRs #3097, #3106, #3116, #3120, #3136, #3138, #3180, #3210, #3214, #3238, #3255, #3261, #3281, #3285, and #3287 each project to 912; open medium-risk PRs #3114, #3168, and #3259 each project to 911; open medium-risk PRs #3095, #3140, and #3230 each project to 910; open medium-risk PR #3326 projects to 905; open medium-risk PR #3079 projects to 904; open high-risk PRs #3157, #3212, #3253, #3351, and #3367 each project to 913; open high-risk PR #3267 projects to 912; open high-risk PR #3075 projects to 910; open high-risk PR #3250 projects to 909; and open high-risk PR #3081 projects to 906. These are not counted in the merged baseline until reviewed and merged.
+Open medium-risk PRs #3070, #3073, #3077, #3089, #3093, #3102, #3108, #3110, #3112, #3118, #3124, #3126, #3128, #3130, #3132, #3134, #3142, #3144, #3146, #3148, #3150, #3152, #3154, #3170, #3176, #3184, #3186, #3188, #3192, #3195, #3198, #3200, #3202, #3206, #3218, #3220, #3226, #3228, #3234, #3240, #3242, #3247, #3257, #3269, #3275, #3277, #3283, #3289, #3292, #3303, #3311, #3314, #3316, #3318, #3322, #3328, #3333, #3338, #3340, #3344, #3347, #3349, #3354, #3356, #3361, and #3365 each project to 913 Effect hook call sites on the current merged baseline; open medium-risk PRs #3097, #3106, #3116, #3120, #3136, #3138, #3180, #3210, #3214, #3238, #3255, #3261, #3281, #3285, #3287, and #3374 each project to 912; open medium-risk PRs #3114, #3168, and #3259 each project to 911; open medium-risk PRs #3095, #3140, and #3230 each project to 910; open medium-risk PR #3326 projects to 905; open medium-risk PR #3079 projects to 904; open high-risk PRs #3157, #3212, #3253, #3351, and #3367 each project to 913; open high-risk PR #3267 projects to 912; open high-risk PR #3075 projects to 910; open high-risk PR #3250 projects to 909; and open high-risk PR #3081 projects to 906. These are not counted in the merged baseline until reviewed and merged.
| Area | Files / signal | Scan status | Notes |
| ------------------------------ | -------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
@@ -63,7 +63,7 @@ Open medium-risk PRs #3070, #3073, #3077, #3089, #3093, #3102, #3108, #3110, #31
| Right sidebar / source control | `components/right-sidebar/**` | Inventory complete, manual review in progress | Source-control selection ref mirrors covered by #3075; Create PR dialog submit reset covered by #3184; commit-failure dialog identity repair covered by #3202; diff-comments clear confirmation repair covered by #3206; diff-notes copy feedback timers covered by #3210; selection stale-key pruning covered by #3212; file-search input focus covered by #3330; file explorer inline create/rename focus covered by #3347. Continue checking polling, PR checks, file explorer watch/reveal, and source-control local resets. Git provider compatibility required. |
| Settings | `components/settings/**` | Inventory complete, manual review in progress | Repository hook local editor visibility covered by #3093; lazy section mounted-state bookkeeping covered by #3128; browser home/Kagi draft sync covered by #3136; general auto-save/Open In draft sync covered by #3138; Source Control AI instruction drafts covered by #3140; notification volume draft sync covered by #3142; repo Source Control AI override draft sync covered by #3144; Quick Commands add intent covered by #3146; Mobile QR polling count capture covered by #3152; integration preflight status derivation covered by #3154; SSH reset-relay dialog cleanup covered by #3157; diagnostics bundle cleanup ref mirror covered by #3176; font autocomplete option scroll covered by #3349; SSH passphrase prompt focus covered by #3351. Continue with draft mirrors while keeping SSH and cross-platform settings behavior intact. |
| Automations | `components/automations/**` | Inventory complete, manual review in progress | Combobox open-focus Effects covered by #3100; run-history selection repair covered by #3104; external run-table selection repair covered by #3102; detail-page selection resets covered by #3106; create-from branch search reset covered by #3132. Continue with draft defaulting, polling, and completion detection. |
-| Issue, PR, task pages | `TaskPage.tsx`, `PullRequestPage.tsx`, `GitHubItemDialog.tsx`, `GitLabItemDialog.tsx`, Linear components | Inventory complete, manual review in progress | Linear estimate input sync covered by #3108; Linear sub-issue list sync covered by #3110; GitHub project date draft sync covered by #3112; GitHub reviewer-picker active index covered by #3114; GitHub project column state covered by #3116; GitHub Project visible-table cache covered by #3186; GitHub Project dialog state covered by #3214; GitLab comment draft reset covered by #3124; Linear title/description draft sync covered by #3130; GitHub assignee dirty-flag reset covered by #3255; GitHub reviewer-request local sync covered by #3259; GitHub link-copy feedback timers covered by #3285; GitLab task filter repair covered by #3292. Continue large-file scan with separate GitHub, GitLab, Linear, and generic review behavior. |
+| Issue, PR, task pages | `TaskPage.tsx`, `PullRequestPage.tsx`, `GitHubItemDialog.tsx`, `GitLabItemDialog.tsx`, Linear components | Inventory complete, manual review in progress | Linear estimate input sync covered by #3108; Linear sub-issue list sync covered by #3110; GitHub project date draft sync covered by #3112; GitHub reviewer-picker active index covered by #3114; GitHub project column state covered by #3116; GitHub Project visible-table cache covered by #3186; GitHub Project dialog state covered by #3214; GitHub Project column preferences covered by #3374; GitLab comment draft reset covered by #3124; Linear title/description draft sync covered by #3130; GitHub assignee dirty-flag reset covered by #3255; GitHub reviewer-request local sync covered by #3259; GitHub link-copy feedback timers covered by #3285; GitLab task filter repair covered by #3292. Continue large-file scan with separate GitHub, GitLab, Linear, and generic review behavior. |
| Onboarding / feature wall | `components/onboarding/**`, `components/feature-wall/**` | Inventory complete, manual review in progress | Select portal root Effects covered by #3087; tour visited-marker Effects covered by #3095; persisted completion close reset covered by #3188; onboarding settings hydration covered by #3198; unused orchestration notifications page deleted by #3273; feature-wall skill installed-state notification Effects covered by #3281; session-depth open publish Effects covered by #3283; shared reduced-motion media-query hook covered by #3287; upstream #3279 feature-tip modal and orchestration visual Effects inspected: CLI command reveal timers and DOM arrow/demo animation stay classified as visual external sync. Continue with demo timers and telemetry while avoiding telemetry semantic changes. |
| Status, dashboard, activity | `components/status-bar/**`, `components/dashboard/**`, `components/activity/**`, `components/pet/**` | Inventory complete, manual review in progress | Status-bar account menu close reset covered by #3051; pet overlay size clamp covered by #3118; Activity selected-thread repair covered by #3126; Workspace Space row-state repairs covered by #3168; Resource Manager Space scan-ready latch covered by #3200; pet reduced-motion subscription covered by #3287; status-bar account usage fetch covered by #3340; Space page open interaction covered by #3361. Check interval sharing, retained agent state, activity terminal portals. |
| Mobile app routes | `mobile/app/**` | Inventory complete, manual review in progress | Pair-confirm route-code parsing covered by #3240; host home new-worktree route action covered by #3242; Accounts unused client ref covered by #3244; mobile home shared-client ref mirror covered by #3247; session route terminal/tab ref mirrors covered by #3250; tasks route client ref mirror covered by #3253; tasks route workspace agent selection covered by #3261. Continue with large `tasks.tsx` state/polling Effects, session route cleanup Effects, home route ref mirrors, and remote-client parity. |
@@ -222,6 +222,7 @@ These are candidate batches, not final conclusions. Each item needs code inspect
| PR EM | Space page open interaction | The Space page recorded workspace-cleanup feature usage from a passive mount Effect after the page rendered. | `WorkspaceSpacePage.tsx`, `ui.ts` covered by #3361 | Medium |
| PR EN | Workspace board open interaction | The workspace board drawer recorded feature usage from a passive open-state Effect after the drawer rendered. | `SidebarHeader.tsx`, `WorkspaceKanbanDrawer.tsx` covered by #3365 | Medium |
| PR EO | Activity terminal portal subscription | Activity terminal portal targets were mirrored from module state through a layout Effect and local state. | `activity-terminal-portal.ts` covered by #3367 | High |
+| PR EP | GitHub Project column preferences | GitHub Project hidden-column and column-width preferences reloaded from localStorage in passive Effects after project/view scope changes. | `ProjectViewList.tsx` covered by #3374 | Medium |
## Merge Risk Scale
@@ -372,6 +373,7 @@ These are candidate batches, not final conclusions. Each item needs code inspect
| #3361 | `nwparker/react-perf-space-page-interaction` | Space page workspace-cleanup interaction records in `openSpacePage` | Medium | Open | `pnpm exec oxfmt --write src/renderer/src/components/workspace-space/WorkspaceSpacePage.tsx src/renderer/src/store/slices/ui.ts src/renderer/src/store/slices/ui.test.ts`; `pnpm exec oxlint src/renderer/src/components/workspace-space/WorkspaceSpacePage.tsx src/renderer/src/store/slices/ui.ts src/renderer/src/store/slices/ui.test.ts`; `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/ui.test.ts`; `pnpm run typecheck:web`; `git diff --check`; AST Effect count 911 -> 910. |
| #3365 | `nwparker/react-perf-workspace-board-open-interaction` | Workspace board feature interaction records in open handlers | Medium | Open | `pnpm exec oxfmt --write src/renderer/src/components/sidebar/SidebarHeader.tsx src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx`; `pnpm exec oxlint src/renderer/src/components/sidebar/SidebarHeader.tsx src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx`; `pnpm run typecheck:web`; `git diff --check`; AST Effect count 911 -> 910. |
| #3367 | `nwparker/react-perf-activity-terminal-portals-store` | Activity terminal portal targets use `useSyncExternalStore` | High | Open | `pnpm exec oxfmt --write src/renderer/src/components/activity/activity-terminal-portal.ts`; `pnpm exec oxlint src/renderer/src/components/activity/activity-terminal-portal.ts`; `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/activity/ActivityPrototypePage.test.ts`; `pnpm run typecheck:web`; `git diff --check`; AST Effect count 911 -> 910. |
+| #3374 | `nwparker/react-perf-project-column-preferences` | GitHub Project column preferences resolve before paint | Medium | Open | `pnpm exec oxfmt --write src/renderer/src/components/github-project/ProjectViewList.tsx`; `pnpm exec oxlint src/renderer/src/components/github-project/ProjectViewList.tsx`; `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/github-project/project-row-filtering.test.ts src/renderer/src/components/github-project/group-sort.test.ts`; `pnpm run typecheck:web`; `git diff --check`; AST Effect count 914 -> 912. |
## Reproduction Commands
From 30a09f3bd9728873592a89f66cfc2371a7eef2f7 Mon Sep 17 00:00:00 2001
From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
Date: Fri, 29 May 2026 19:39:52 -0400
Subject: [PATCH 027/224] Add mobile terminal shortcut bar customization
(#3012)
Co-authored-by: Orca
---
.gitignore | 1 +
mobile/app/_layout.tsx | 30 +-
.../app/h/[hostId]/session/[worktreeId].tsx | 104 +-
mobile/app/pair-confirm.tsx | 24 +-
mobile/app/pair-scan.tsx | 4 +-
mobile/app/pair.tsx | 87 +
mobile/app/terminal-settings.tsx | 252 +-
mobile/mock-homepage.html | 2285 +++++++-----
mobile/mock-tasks.html | 3193 ++++++++++-------
mobile/src/components/CustomKeyModal.tsx | 15 +-
.../terminal/terminal-accessory-keys.test.ts | 35 +-
.../src/terminal/terminal-accessory-keys.ts | 62 +-
.../terminal-accessory-layout.test.ts | 142 +
.../src/terminal/terminal-accessory-layout.ts | 137 +
mobile/src/transport/pairing.test.ts | 21 +
mobile/src/transport/pairing.ts | 29 +-
src/cli/help.ts | 2 +-
src/cli/runtime/client.ts | 2 +-
src/cli/runtime/websocket-transport.test.ts | 5 +-
src/cli/specs/environment.ts | 2 +-
.../settings/RuntimeEnvironmentsPane.tsx | 2 +-
src/renderer/src/web/WebConnect.tsx | 2 +-
src/renderer/src/web/web-pairing.test.ts | 27 +
src/renderer/src/web/web-pairing.ts | 9 +-
src/shared/pairing.test.ts | 16 +-
src/shared/pairing.ts | 34 +-
src/shared/runtime-environment-store.ts | 2 +-
src/shared/telemetry-events.test.ts | 2 +
28 files changed, 4103 insertions(+), 2423 deletions(-)
create mode 100644 mobile/app/pair.tsx
create mode 100644 mobile/src/terminal/terminal-accessory-layout.test.ts
create mode 100644 mobile/src/terminal/terminal-accessory-layout.ts
create mode 100644 mobile/src/transport/pairing.test.ts
create mode 100644 src/renderer/src/web/web-pairing.test.ts
diff --git a/.gitignore b/.gitignore
index 1078c5105dd..de8a6eb8af3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -81,6 +81,7 @@ docs/**
!docs/reference/
!docs/reference/**
!docs/STYLEGUIDE.md
+!docs/mobile-terminal-shortcut-bar.md
# Stably CLI (only docs/ are tracked)
.stably/*
diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx
index 188d52fe604..e6e091edf72 100644
--- a/mobile/app/_layout.tsx
+++ b/mobile/app/_layout.tsx
@@ -10,6 +10,7 @@ import { OrcaLogo } from '../src/components/OrcaLogo'
import { RpcClientProvider } from '../src/transport/client-context'
import { getNotificationNavigationPath } from '../src/notifications/notification-routing'
import { loadHosts } from '../src/transport/host-store'
+import { extractPairingCodeFromUrl } from '../src/transport/pairing'
// Why: keeps the native splash screen visible until the React tree is mounted
// and ready to render. Without this the user sees a blank white/black frame
@@ -30,39 +31,23 @@ Notifications.setNotificationHandler({
})
})
-// Why: extract the path+payload that follows the orca://pair anchor so we
-// can route it to the confirm screen. Accept either a hash payload
-// (`orca://pair#`, the QR / shared form) or a query param
-// (`orca://pair?code=<...>`, future-proof for share sheets that strip
-// fragments).
-function extractPairCode(url: string): string | null {
- if (!url.startsWith('orca://pair')) return null
- const hashIndex = url.indexOf('#')
- if (hashIndex !== -1) {
- return url.slice(hashIndex + 1) || null
- }
- const queryIndex = url.indexOf('?')
- if (queryIndex !== -1) {
- const params = new URLSearchParams(url.slice(queryIndex + 1))
- return params.get('code')
- }
- return null
-}
-
export default function RootLayout() {
const router = useRouter()
const handledNotificationIdsRef = useRef>(new Set())
- // Why: route `orca://pair#` deep links to the confirm screen so
+ // Why: route `orca://pair?...` deep links to the confirm screen so
// the same pairing flow runs whether the link arrived via QR scan,
// paste, AirDrop, Messages, or `xcrun simctl openurl`. getInitialURL
// covers cold-start (link tapped while app was closed); the listener
// covers warm-start (link tapped while app is in memory).
useEffect(() => {
function handleUrl(url: string) {
- const code = extractPairCode(url)
+ const code = extractPairingCodeFromUrl(url)
if (code) {
- router.push({ pathname: '/pair-confirm', params: { code } })
+ // Why: Android camera launches can leave Expo Router's unmatched
+ // `orca://pair` route underneath this screen; replacing keeps cancel
+ // and edge-back from revealing the router error page.
+ router.replace({ pathname: '/pair-confirm', params: { code } })
}
}
@@ -169,6 +154,7 @@ export default function RootLayout() {
}}
/>
+
diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx
index 5bd76d65e03..05a41c98775 100644
--- a/mobile/app/h/[hostId]/session/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx
@@ -18,13 +18,14 @@ import {
type TextStyle
} from 'react-native'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useLocalSearchParams, useRouter } from 'expo-router'
+import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'
import AsyncStorage from '@react-native-async-storage/async-storage'
import {
AlertTriangle,
ArrowUp,
ChevronLeft,
ChevronRight,
+ ChevronsRight,
Eraser,
Folder,
File,
@@ -59,7 +60,11 @@ import {
type TerminalModes,
type TerminalWebViewHandle
} from '../../../../src/terminal/TerminalWebView'
-import { TERMINAL_ACCESSORY_KEYS } from '../../../../src/terminal/terminal-accessory-keys'
+import {
+ getDefaultTerminalAccessoryBuiltInIds,
+ getVisibleTerminalAccessoryKeys,
+ loadTerminalAccessoryLayout
+} from '../../../../src/terminal/terminal-accessory-layout'
import {
getTerminalLiveSpecialKeyBytes,
isTerminalLiveInputWithinByteLimit
@@ -724,8 +729,15 @@ export default function SessionScreen() {
const [leaveDrafts, setLeaveDrafts] = useState(null)
const [renameTarget, setRenameTarget] = useState(null)
const [customKeys, setCustomKeys] = useState([])
+ const [visibleBuiltInIds, setVisibleBuiltInIds] = useState(
+ getDefaultTerminalAccessoryBuiltInIds
+ )
const [showCustomKeyModal, setShowCustomKeyModal] = useState(false)
const [deleteKeyTarget, setDeleteKeyTarget] = useState(null)
+ const visibleBuiltInAccessoryKeys = useMemo(
+ () => getVisibleTerminalAccessoryKeys(visibleBuiltInIds),
+ [visibleBuiltInIds]
+ )
// Why: in Expo SDK 55 edge-to-edge mode the OS does NOT resize the window when
// the IME opens — the keyboard draws on top of the app. We track the keyboard
// height ourselves and translate the input/accessory area above the IME without
@@ -1716,6 +1728,34 @@ export default function SessionScreen() {
void loadCustomKeys().then(setCustomKeys)
}, [])
+ useFocusEffect(
+ useCallback(() => {
+ let stale = false
+ void loadTerminalAccessoryLayout().then((layout) => {
+ if (!stale) setVisibleBuiltInIds(layout.visibleBuiltInIds)
+ })
+ return () => {
+ stale = true
+ }
+ }, [])
+ )
+
+ useEffect(() => {
+ let mounted = true
+ const refresh = () => {
+ void loadTerminalAccessoryLayout().then((layout) => {
+ if (mounted) setVisibleBuiltInIds(layout.visibleBuiltInIds)
+ })
+ }
+ const sub = AppState.addEventListener('change', (s: AppStateStatus) => {
+ if (s === 'active') refresh()
+ })
+ return () => {
+ mounted = false
+ sub.remove()
+ }
+ }, [])
+
// Why: re-measure when non-keyboard layout-affecting state changes
// (e.g. tab strip toggling visibility when the terminal count crosses
// 0↔1 — without this, a freshly-created 2nd tab subscribes with a
@@ -1815,6 +1855,11 @@ export default function SessionScreen() {
[customKeys]
)
+ const handleManageShortcuts = useCallback(() => {
+ setShowCustomKeyModal(false)
+ router.push('/terminal-settings')
+ }, [router])
+
useEffect(() => {
clearTerminalCache()
activeHandleRef.current = null
@@ -3279,15 +3324,16 @@ export default function SessionScreen() {
: 'Switch to live terminal input'
}
>
-
- Live
-
+
{canPaste && (
)}
- {TERMINAL_ACCESSORY_KEYS.map((key) => (
+ {visibleBuiltInAccessoryKeys.map((key) => (
[
styles.accessoryKey,
pressed && styles.accessoryKeyPressed,
@@ -3383,12 +3429,9 @@ export default function SessionScreen() {
onPress={focusLiveInput}
accessibilityLabel="Focus live terminal input"
>
-
-
- Live
-
+
- Keyboard input goes to terminal
+ Keyboard input directly goes to terminal
setShowCustomKeyModal(false)}
onKeysChanged={setCustomKeys}
+ onManageShortcuts={handleManageShortcuts}
/>
([])
+ const cancel = useCallback(() => {
+ router.replace('/')
+ }, [router])
+
+ useFocusEffect(
+ useCallback(() => {
+ const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
+ cancel()
+ return true
+ })
+ return () => subscription.remove()
+ }, [cancel])
+ )
+
useEffect(() => {
if (!params.code) {
setStatus('error')
@@ -118,10 +132,6 @@ export default function PairConfirmScreen() {
}
}
- function cancel() {
- router.replace('/')
- }
-
const containerPadding = { paddingTop: insets.top + spacing.sm }
return (
diff --git a/mobile/app/pair-scan.tsx b/mobile/app/pair-scan.tsx
index ca8a8673e7a..b64ea284a87 100644
--- a/mobile/app/pair-scan.tsx
+++ b/mobile/app/pair-scan.tsx
@@ -210,7 +210,7 @@ export default function PairScanScreen() {
visible={pasteVisible}
title="Paste pairing code"
message="Copy the code shown under the QR on your computer."
- placeholder="orca://pair#... or paste the code"
+ placeholder="orca://pair?code=... or paste the code"
onSubmit={handlePasteSubmit}
onCancel={() => setPasteVisible(false)}
/>
@@ -306,7 +306,7 @@ export default function PairScanScreen() {
visible={pasteVisible}
title="Paste pairing code"
message="Copy the code shown under the QR on your computer."
- placeholder="orca://pair#... or paste the code"
+ placeholder="orca://pair?code=... or paste the code"
onSubmit={handlePasteSubmit}
onCancel={() => setPasteVisible(false)}
/>
diff --git a/mobile/app/pair.tsx b/mobile/app/pair.tsx
new file mode 100644
index 00000000000..6f3483763e1
--- /dev/null
+++ b/mobile/app/pair.tsx
@@ -0,0 +1,87 @@
+import { useCallback, useEffect, useState } from 'react'
+import { ActivityIndicator, Linking, Pressable, StyleSheet, Text, View } from 'react-native'
+import { useLocalSearchParams, useRouter } from 'expo-router'
+import { colors, radii, spacing, typography } from '../src/theme/mobile-theme'
+import { extractPairingCodeFromUrl } from '../src/transport/pairing'
+
+export default function PairRedirectScreen() {
+ const router = useRouter()
+ const params = useLocalSearchParams<{ code?: string }>()
+ const [missingCode, setMissingCode] = useState(false)
+
+ const goHome = useCallback(() => {
+ router.replace('/')
+ }, [router])
+
+ useEffect(() => {
+ let disposed = false
+
+ async function redirectToConfirm() {
+ const codeParam = Array.isArray(params.code) ? params.code[0] : params.code
+ if (codeParam) {
+ router.replace({ pathname: '/pair-confirm', params: { code: codeParam } })
+ return
+ }
+
+ const initialUrl = await Linking.getInitialURL().catch(() => null)
+ const code = initialUrl ? extractPairingCodeFromUrl(initialUrl) : null
+ if (disposed) {
+ return
+ }
+ if (code) {
+ router.replace({ pathname: '/pair-confirm', params: { code } })
+ return
+ }
+ setMissingCode(true)
+ }
+
+ void redirectToConfirm()
+ return () => {
+ disposed = true
+ }
+ }, [params.code, router])
+
+ return (
+
+ {missingCode ? (
+ <>
+ Missing pairing code
+
+ Back to home
+
+ >
+ ) : (
+
+ )}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: colors.bgBase,
+ padding: spacing.lg
+ },
+ errorText: {
+ color: colors.statusRed,
+ fontSize: typography.bodySize,
+ lineHeight: 20,
+ marginBottom: spacing.xl,
+ textAlign: 'center'
+ },
+ primaryButton: {
+ alignItems: 'center',
+ backgroundColor: colors.textPrimary,
+ borderRadius: radii.button,
+ paddingHorizontal: spacing.xl,
+ paddingVertical: spacing.sm + 2
+ },
+ primaryButtonText: {
+ color: colors.bgBase,
+ fontSize: typography.bodySize,
+ fontWeight: '600'
+ }
+})
diff --git a/mobile/app/terminal-settings.tsx b/mobile/app/terminal-settings.tsx
index abcf324a855..107ed53f925 100644
--- a/mobile/app/terminal-settings.tsx
+++ b/mobile/app/terminal-settings.tsx
@@ -1,14 +1,40 @@
-import { useEffect, useMemo, useState } from 'react'
-import { View, Text, StyleSheet, Pressable, ScrollView } from 'react-native'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import {
+ AppState,
+ View,
+ Text,
+ StyleSheet,
+ Pressable,
+ ScrollView,
+ Switch,
+ type AppStateStatus
+} from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useRouter } from 'expo-router'
-import { ChevronLeft, ChevronRight, Smartphone } from 'lucide-react-native'
-import { colors, spacing, typography } from '../src/theme/mobile-theme'
+import { useFocusEffect, useRouter } from 'expo-router'
+import { ChevronLeft, ChevronRight, Smartphone, X } from 'lucide-react-native'
+import {
+ CustomKeyModal,
+ loadCustomKeys,
+ saveCustomKeys,
+ type CustomKey
+} from '../src/components/CustomKeyModal'
+import { colors, radii, spacing, typography } from '../src/theme/mobile-theme'
import { loadHosts } from '../src/transport/host-store'
import type { HostProfile } from '../src/transport/types'
import { useAllHostClients } from '../src/transport/client-context'
import type { RpcClient } from '../src/transport/rpc-client'
import { PickerModal, type PickerOption } from '../src/components/PickerModal'
+import {
+ TERMINAL_ACCESSORY_KEYS,
+ type TerminalAccessoryKey
+} from '../src/terminal/terminal-accessory-keys'
+import {
+ getDefaultTerminalAccessoryBuiltInIds,
+ loadTerminalAccessoryLayout,
+ resetTerminalAccessoryBuiltInIds,
+ saveTerminalAccessoryLayout,
+ setTerminalAccessoryBuiltInVisible
+} from '../src/terminal/terminal-accessory-layout'
type RestoreValue = 'indefinite' | '60s' | '5m' | '30m'
@@ -74,6 +100,33 @@ function HostFitRow({
)
}
+function ShortcutBarRow({
+ shortcutKey,
+ visible,
+ onToggle
+}: {
+ shortcutKey: TerminalAccessoryKey
+ visible: boolean
+ onToggle: (visible: boolean) => void
+}): React.JSX.Element {
+ return (
+
+
+ {shortcutKey.label}
+
+
+ {shortcutKey.accessibilityLabel ?? shortcutKey.label}
+
+
+
+ )
+}
+
export default function TerminalSettingsScreen() {
const router = useRouter()
const insets = useSafeAreaInsets()
@@ -84,6 +137,9 @@ export default function TerminalSettingsScreen() {
const hostIds = useMemo(() => hosts.map((h) => h.id), [hosts])
const hostClients = useAllHostClients(hostIds)
+ const [customKeys, setCustomKeys] = useState([])
+ const [showCustomKeyModal, setShowCustomKeyModal] = useState(false)
+
// Why: per-host current value, lazily fetched. We keep state at the
// screen level rather than per-row so the picker can render at root
// level — embedding PickerModal inside a row clipped its BottomDrawer
@@ -91,6 +147,79 @@ export default function TerminalSettingsScreen() {
// drawer appear cut-off.
const [hostMs, setHostMs] = useState>({})
const [pickerHostId, setPickerHostId] = useState(null)
+ const [visibleBuiltInIds, setVisibleBuiltInIds] = useState(
+ getDefaultTerminalAccessoryBuiltInIds
+ )
+ const layoutWriteChainRef = useRef>(Promise.resolve())
+ const layoutWriteSeqRef = useRef(0)
+ const pendingLayoutWritesRef = useRef(0)
+
+ const persistLayout = useCallback((nextIds: string[]) => {
+ layoutWriteSeqRef.current += 1
+ pendingLayoutWritesRef.current += 1
+ layoutWriteChainRef.current = layoutWriteChainRef.current
+ .catch(() => {})
+ .then(() => saveTerminalAccessoryLayout(nextIds))
+ .catch(() => {})
+ .finally(() => {
+ pendingLayoutWritesRef.current -= 1
+ })
+ }, [])
+
+ const refreshShortcutLayout = useCallback(() => {
+ const refreshSeq = layoutWriteSeqRef.current
+ void loadTerminalAccessoryLayout().then((layout) => {
+ if (pendingLayoutWritesRef.current > 0 || refreshSeq !== layoutWriteSeqRef.current) return
+ setVisibleBuiltInIds(layout.visibleBuiltInIds)
+ })
+ }, [])
+
+ const refreshCustomKeys = useCallback(() => {
+ void loadCustomKeys().then(setCustomKeys)
+ }, [])
+
+ const handleDeleteCustomKey = useCallback(
+ async (key: CustomKey) => {
+ const updated = customKeys.filter((k) => k.id !== key.id)
+ setCustomKeys(updated)
+ await saveCustomKeys(updated)
+ },
+ [customKeys]
+ )
+
+ useFocusEffect(
+ useCallback(() => {
+ refreshShortcutLayout()
+ refreshCustomKeys()
+ }, [refreshShortcutLayout, refreshCustomKeys])
+ )
+
+ useEffect(() => {
+ const sub = AppState.addEventListener('change', (s: AppStateStatus) => {
+ if (s === 'active') {
+ refreshShortcutLayout()
+ refreshCustomKeys()
+ }
+ })
+ return () => sub.remove()
+ }, [refreshShortcutLayout, refreshCustomKeys])
+
+ const toggleBuiltInKey = useCallback(
+ (id: string, visible: boolean) => {
+ setVisibleBuiltInIds((current) => {
+ const next = setTerminalAccessoryBuiltInVisible(current, id, visible)
+ persistLayout(next)
+ return next
+ })
+ },
+ [persistLayout]
+ )
+
+ const resetBuiltInKeys = useCallback(() => {
+ const next = resetTerminalAccessoryBuiltInIds()
+ setVisibleBuiltInIds(next)
+ persistLayout(next)
+ }, [persistLayout])
useEffect(() => {
let cancelled = false
@@ -143,6 +272,7 @@ export default function TerminalSettingsScreen() {
}
const pickerHost = pickerHostId ? hosts.find((h) => h.id === pickerHostId) : null
+ const visibleBuiltInSet = useMemo(() => new Set(visibleBuiltInIds), [visibleBuiltInIds])
return (
@@ -186,6 +316,76 @@ export default function TerminalSettingsScreen() {
})}
)}
+
+ SHORTCUT BAR
+
+ {TERMINAL_ACCESSORY_KEYS.map((shortcutKey, idx) => (
+
+ {idx > 0 && }
+ toggleBuiltInKey(shortcutKey.id, visible)}
+ />
+
+ ))}
+
+ [styles.row, pressed && styles.rowPressed]}
+ onPress={resetBuiltInKeys}
+ >
+
+ Reset Defaults
+ Show every built-in shortcut key
+
+
+
+
+ CUSTOM SHORTCUTS
+
+ {customKeys.length === 0 ? (
+
+ No custom shortcuts defined yet.
+
+ ) : (
+ customKeys.map((key, idx) => (
+
+ {idx > 0 && }
+
+
+ {key.label}
+
+
+ {key.label}
+
+ {key.bytes.replace(/\r/g, ' ↵')}
+
+
+ [
+ styles.deleteButton,
+ pressed && styles.deleteButtonPressed
+ ]}
+ onPress={() => handleDeleteCustomKey(key)}
+ >
+
+
+
+
+ ))
+ )}
+
+ [styles.row, pressed && styles.rowPressed]}
+ onPress={() => setShowCustomKeyModal(true)}
+ >
+
+ Add Custom Shortcut…
+ Create key combo or text macro
+
+
+
+
@@ -198,6 +398,14 @@ export default function TerminalSettingsScreen() {
}}
onClose={() => setPickerHostId(null)}
/>
+
+ setShowCustomKeyModal(false)}
+ onKeysChanged={(keys) => {
+ setCustomKeys(keys)
+ }}
+ />
)
}
@@ -239,6 +447,9 @@ const styles = StyleSheet.create({
marginBottom: spacing.xs,
paddingHorizontal: spacing.xs
},
+ groupTopGap: {
+ marginTop: spacing.xl
+ },
groupDescription: {
fontSize: typography.bodySize - 1,
color: colors.textSecondary,
@@ -247,7 +458,7 @@ const styles = StyleSheet.create({
},
section: {
backgroundColor: colors.bgPanel,
- borderRadius: 12,
+ borderRadius: radii.card,
overflow: 'hidden'
},
sectionTopGap: {
@@ -281,9 +492,38 @@ const styles = StyleSheet.create({
color: colors.textSecondary,
marginTop: 2
},
+ keycap: {
+ minWidth: 62,
+ alignItems: 'center',
+ backgroundColor: colors.bgRaised,
+ borderRadius: radii.button,
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs
+ },
+ keycapText: {
+ color: colors.textSecondary,
+ fontSize: typography.metaSize,
+ fontFamily: typography.monoFamily
+ },
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.borderSubtle,
marginHorizontal: spacing.md
+ },
+ emptyContainer: {
+ padding: spacing.md,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ deleteButton: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: 'rgba(239, 68, 68, 0.1)'
+ },
+ deleteButtonPressed: {
+ backgroundColor: 'rgba(239, 68, 68, 0.2)'
}
})
diff --git a/mobile/mock-homepage.html b/mobile/mock-homepage.html
index b541dbb0a95..fe8301fd9bd 100644
--- a/mobile/mock-homepage.html
+++ b/mobile/mock-homepage.html
@@ -1,999 +1,1374 @@
-
+
-
-
-
-Orca Mobile – Homepage Redesign
-
-
-
-
-
- Current
- Task Entry
- Empty State
-
-
-
-
-
-
-
-
-
-
-
-
-
1d 14h
-
Agent time
-
-
-
-
-
Desktops
-
-
-
-
-
-
-
-
-
+ .empty-steps {
+ width: 100%;
+ padding: 0 24px 40px;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ }
+ .empty-step {
+ display: flex;
+ align-items: flex-start;
+ gap: 14px;
+ padding: 16px 0;
+ }
+ .empty-step + .empty-step {
+ border-top: 1px solid var(--border-subtle);
+ }
+ .step-num {
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border-subtle);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--text-secondary);
+ flex-shrink: 0;
+ margin-top: 1px;
+ }
+ .step-text {
+ flex: 1;
+ }
+ .step-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 3px;
+ }
+ .step-desc {
+ font-size: 12px;
+ color: var(--text-muted);
+ line-height: 1.4;
+ }
+
+
+
+
+ Current
+ Task Entry
+ Empty State
-
-
-
-
-
-
Pair another desktop
-
Scan a QR code from Orca desktop
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
1d 14h
-
Agent time
-
-
-
-
-
Tasks
-
-
-
-
-
Task inbox
-
-
Best candidate: puts issues/PRs at the same hierarchy as desktops without overloading Quick Actions.
-
-
Desktops
-