Skip to content

fix: stabilize PR272 new-session UX, profiles gating, and env safety#12

Closed
leeroybrun wants to merge 0 commit intomainfrom
slopus/pr/upstream-sync-regressions-2026-01-12
Closed

fix: stabilize PR272 new-session UX, profiles gating, and env safety#12
leeroybrun wants to merge 0 commit intomainfrom
slopus/pr/upstream-sync-regressions-2026-01-12

Conversation

@leeroybrun
Copy link
Collaborator

@leeroybrun leeroybrun commented Jan 16, 2026

Context / Goal

This PR is a follow-up to slopus/happy#272, which introduced a large new-session + profiles + env-var/settings/sync surface.
The goal here is to keep the feature work, but restore the pre-#272 “standard New Session” UX and make the new profiles/env work coherent + safe.

I’m trying to keep this factual (no blame): this is mostly stabilization work after a very large merge.

Summary (what this PR does)

  • Restores the pre-#272 “New Session” experience (modal + prompt-first workflow) while keeping the new wizard/profiles as opt-in.
  • Keep PR272’s new features, but make them optional + coherent
    • Added separate feature settings: users can choose to use profiles with "standard" new session screen and/or new wizard independently of eachother
    • Reduce duplication by reusing existing UI primitives
    • Make search for machines & paths opt-in in features settings
  • Fix key non-UI regressions / safety issues introduced or made risky by PR272 (tool results, settings parsing, logging, env var handling)
  • Makes profile editing/navigation unmount-safe and prevents invalid profiles from being persisted.
  • Removes dead/duplicative env resolution work and ensures env previews do not fetch secret values into UI memory.
  • Cleans up i18n structure by separating translation types from translation content.
  • Wizard improvements/fixes
    • Reuse existing UI components instead of duplicated ones introduced by #272
    • Improves search & favorites display in wizard
    • Add favorites feature for profiles
    • Add AI Backend wizard step
    • Codex profiles correctly enabled if Codex is detected on machine (see below)
    • Add optional AI Model selection step
      • Right now, only Gemini, as we only support changing AI model for Gemini (following #376 merge)
  • Profiles improvements/fixes
    • Allow to select which AI Backend applies to a profile in the profile settings
  • Integrate profiles in standard "new session" modal (opt-in, gated by feature setting)
    • Add a chip + picker for profiles
    • Add a chip + modal for env vars preview
    • Add clear message to AI Backend chip if the user clicks on it but cannot change it because of the currently selected profile
      • Previously nothing would happen when users clicked on the chip and that the profile did not allow AI backend change

What #272 introduced (and what we’re fixing here)

Profiles/editor correctness

  • #272’s profile edit screen could create/persist an invalid profile with id: ''.
    • Fixed via id-based navigation + UUID-backed “empty profile” creation

Navigation coherence (web + native + unmount safety)

  • #272 used a hybrid approach (module-level callbacks + URL-serialized JSON) for profile editing, while other pickers used params.
  • Fixed by switching profile edit to id-based routing and removing the callback/URL-JSON pattern.

Global daemon env fetch (and secret exposure risk)

  • #272 introduced a global envVarRefs/daemonEnv fetch in /new that queried the daemon for all ${VAR} references across all profiles (including secrets).
  • This can pull secret values into JS memory unnecessarily (even if only used for small previews).
  • Fixed by removing the global query and keeping env resolution scoped with secret-like filtering in preview paths.

New Session (standard flow) regressions

  • New Session was effectively treated as a screen rather than the prior modal presentation.
  • The prompt-first input area was changed to show an oversized “context card” (machine/path), losing the compact chip UI.
  • Placeholder/keyboard offset behavior drifted from the pre-merge UX.

Wizard + pickers inconsistent with existing UI

  • Wizard sections (working directory, permission mode) and pickers (machine/path) used new layouts/styles that didn’t match existing Happy patterns and duplicated logic.

Profiles “leaked” into default behavior

  • Profiles & env vars could affect session spawning, even when disabled in the settings
  • Profiles management UI diverged from existing settings layout conventions.

Non-UI safety/reliability issues (PR272 logic surface)

  • Sensitive settings/logging risk: decrypted settings can now include profile env vars / keys; existing logs became unsafe.
  • Tool result dropping risk: PR272’s message normalization could reject tool results when output is structured JSON (common for Codex/Gemini), causing messages to be dropped.
  • Settings parse fragility: “whole settings parse fails → defaults” becomes much more dangerous once settings include user-edited profiles/env vars.
  • New env-var querying hooks had edge cases (whitespace trimming, unset vs empty ambiguity).
  • Dead/placeholder code added (e.g. profileSync.ts, `NewSessionWizard) that wasn’t actually wired.

Even if Codex was detected on the machines, the Codex profiles stayed disabled

  • We can even see it in the PR #272 demo itself:
    IMAGE

What we implemented to fix/improve it (what’s in this branch)

1) New Session UX restored (pre-PR272 feel, still compatible with new features)

  • Restored modal presentation for New Session.
  • Restored prompt-first “standard” flow layout and compact chips.
  • Restored original translated placeholder (t('session.inputPlaceholder')) and legacy keyboard behavior.

2) Wizard + pickers refactored to reduce duplication and improve coherence

  • Extracted reusable selectors and used them in both:
    • wizard flow
    • modal pickers

3) Profiles made truly optional (separate feature flag) + coherent selection UX

  • Introduced independent useProfiles flag (separate from useEnhancedSessionWizard).
  • Gated:
    • profiles settings entry
    • profiles UI in New Session (standard and wizard)
    • profile selection/persistence logic
    • profile env var injection
  • Added a lightweight profile picker modal for standard flow (chip opens picker; no forced wizard).

4) Profile identity persisted/displayed safely

  • Sessions now persist profileId and show a read-only chip in session UI.
  • Handles missing/unknown profile IDs gracefully.

Non-UI “logic hardening” improvements tied to PR272’s new surface area

These changes are limited to files added/modified by PR272, and fix real issues introduced or made risky by PR272’s new data/features.

A) Prevent leaking secrets via logs

  • Removed/limited logging of full decrypted settings (can contain API keys / profile env vars).
  • Avoid logging raw full message payloads on validation errors.
  • Gated noisy CLI detection logs to __DEV__.

B) Fix tool-result robustness (don’t drop messages; don’t break on structured outputs)

  • Tool results can be strings or structured objects (esp. Codex/Gemini). PR272’s schema/normalization could reject these, dropping messages.
  • Updated tool-result parsing/normalization so non-string outputs are accepted and safely stringified for display.

C) Make settings parsing tolerant (avoid “one bad profile nukes all settings”)

  • PR272 expanded settings and added profiles/env-var editing. A single invalid field should not reset known settings to defaults.
  • Updated settingsParse() to parse fields individually and filter invalid profile entries rather than failing the entire settings object.

D) Unify env-var template parsing + preserve correctness in remote env querying

  • Standardized supported template syntax to ${VAR} and ${VAR:-default} consistently.
  • Improved env var fetching:
    • avoid trimming stdout (which can corrupt values)
    • distinguish unset vs empty string using a JSON protocol via node when available (fallback remains safe)
    • fixed a TS template-literal escape bug in fallback

E) Remove unused placeholder sync service added by PR272

  • profileSync.ts was added but not referenced; removed to prevent future confusion.

F) Conservative security: avoid querying remote values for secret-like vars

  • Strengthened secret heuristics to include PASS|PASSWORD|COOKIE in addition to token/key/secret/auth.
  • This prevents accidental remote querying/display of sensitive env vars.

Links to the detailled issues in slopus/happy#272 (links go to the PR diff)

  1. /new is no longer presented as a modal (becomes a normal screen)
  1. Prompt placeholder is hardcoded (loses i18n key)
  1. Profiles/env injection is on the default spawn path (no opt-in gate)
  1. Global daemon env fetch in /new queries ${VAR} refs across all profiles (no secret filtering)
  1. Profile editor env-var cards tell you to “select a machine”, but Profiles settings editor has no machine picker
  1. Secret-handling messaging mismatch
  1. Profile editor route can create a new profile with id: ''
  1. Profile edit navigation uses URL-serialized JSON payloads + module-level callbacks (unmount/deeplink fragile)
  1. Startup Bash Script is stored/edited, but not part of spawn RPC (unwired)
  1. CLI detection + profile availability gating can false-negative Codex/Gemini
  1. Tool-result normalization can drop structured tool outputs
  1. Settings parsing is “whole-object” safeParse (invalid settings fall back to defaults for all known fields)
  1. useEnvironmentVariables trims stdout + collapses empty-string to null
  1. profileSync.ts is effectively a placeholder (TODOs for actual daemon RPCs)
  1. tmux “empty sessionName means current/most recent” is stated in UI
  1. i18n English duplication (“dedicated en file” + tooling imports it)
  1. Large wizard component introduced but unused

Summary by CodeRabbit

  • New Features

    • Profile manager with favorites, create/edit/duplicate/delete flows; profile picker & editor screens
    • Improved machine & path pickers: search, recent/favorites, persistent toggles
    • Environment variables editor, preview modal and machine-backed resolution
    • Gemini model-mode selection exposed in UI; new Gemini tool views and model options
  • Refactor

    • Modularized picker/item UI, reusable search header and action menus; web/modal behavior improvements
  • Bug Fixes

    • Tighter env-var substitution and more robust resolution; improved permission-mode mapping across agents
  • Tests & Docs

    • New unit tests and expanded translations across many languages

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

Adds profile management and favorites, Gemini model-mode & permission typing and UI wiring, reusable machine/path pickers with search/favorites, machine-backed environment‑variable preview/editing, modal typing changes, extensive i18n updates, new UI components, tests, and removal of the old NewSessionWizard and ProfileSync service.

Changes

Cohort / File(s) Summary
Profile system & editors
\sources/app/(app)/new/pick/profile.tsx`, `sources/app/(app)/new/pick/profile-edit.tsx`, `sources/app/(app)/settings/profiles.tsx`, `sources/sync/profileGrouping.ts`, `sources/sync/profileMutations.ts`, `sources/sync/profileUtils.ts``
New ProfilePicker/Edit screens (memoized), favorites, duplicate/convert flows, unsaved-change prompts, grouping helpers, and profile mutation utilities; UI and persistence plumbing added.
Machine & Path pickers / selectors
\sources/app/(app)/new/pick/machine.tsx`, `sources/app/(app)/new/pick/path.tsx`, `sources/components/newSession/MachineSelector.tsx`, `sources/components/newSession/PathSelector.tsx``
Replace inline pickers with MachineSelector/PathSelector, persistent picker settings, recent/favorites, search integration, and navigation-param selection handling.
Gemini model & permission support
\sources/-session/SessionView.tsx`, `sources/components/AgentInput.tsx`, `sources/sync/modelOptions.ts`, `sources/sync/permissionTypes.ts`, `sources/sync/permissionMapping.ts`, `sources/sync/storageTypes.ts`, `sources/sync/storage.ts`, `sources/sync/persistence.ts`, `sources/sync/ops.ts`, `sources/sync/sync.ts``
Introduces PermissionMode/ModelMode types, agent-aware model options, mapping across agents, session.modelMode wiring & persistence (defaults for Gemini), and spawn payload profileId wiring.
Environment variables & previews
\sources/components/EnvironmentVariableCard.tsx`, `sources/components/EnvironmentVariablesList.tsx`, `sources/components/newSession/EnvironmentVariablesPreviewModal.tsx`, `sources/hooks/useEnvironmentVariables.ts`, `sources/utils/envVarTemplate.ts`, `sources/utils/envVarTemplate.test.ts``
Template parsing/formatting, machine-backed env var resolution (JSON-first, shell fallback), per-variable machine preview/status, and UI for editing/previewing env vars.
Modal, menu, actions, search UI
\sources/modal/types.ts`, `sources/modal/ModalManager.ts`, `sources/modal/components/CustomModal.tsx`, `sources/components/ItemActionsMenuModal.tsx`, `sources/components/ItemRowActions.tsx`, `sources/components/SearchHeader.tsx``
Generic custom-modal typing with injected onClose, typed Modal.show signature, prop coercion in CustomModal, new ItemActionsMenuModal and ItemRowActions overflow menu, and a reusable SearchHeader component.
Tools / Gemini tooling & ACP
\sources/components/tools/knownTools.tsx`, `sources/components/tools/views/GeminiEditView.tsx`, `sources/components/tools/views/GeminiExecuteView.tsx`, `sources/components/tools/views/_all.tsx`, `sources/sync/typesRaw.ts`, `sources/sync/typesRaw.spec.ts``
Adds Gemini tool entries and views plus ACP (Agent Communication Protocol) parsing/normalization for tool-call/tool-result and related event types.
UI components & selection helpers
\sources/components/ItemGroup.tsx`, `sources/components/ItemGroup.dividers.ts`, `sources/components/Item.tsx`, `sources/components/SessionTypeSelector.tsx`, `sources/components/Switch.web.tsx`, `sources/components/SearchableListSelector.tsx`, `sources/components/profileActions.ts``
ItemGroup selection context + divider helper, Item selected-background logic, SessionTypeSelector refactor, web Switch, SearchableListSelector API updates, and profile action builder.
Removed legacy sync & wizard
\sources/sync/profileSync.ts`(removed),`sources/components/NewSessionWizard.tsx`(removed),`sources/components/PermissionModeSelector.tsx`` (removed)
Removes ProfileSync singleton service, the NewSessionWizard component, and the standalone PermissionModeSelector (functionality integrated elsewhere).
Settings, feature flags & session info
\sources/app/(app)/settings/features.tsx`, `sources/components/SettingsView.tsx`, `sources/app/(app)/session/[id]/info.tsx`, `sources/app/(app)/_layout.tsx``
New experimental toggles (useProfiles, useMachinePickerSearch, usePathPickerSearch), conditional profile UI, session info shows resolved profile label, and header/screen option adjustments.
Storage, persistence & metadata
\sources/sync/persistence.ts`, `sources/sync/persistence.test.ts`, `sources/sync/storage.ts`, `sources/sync/storageTypes.ts`, `sources/sync/messageMeta.ts`, `sources/sync/messageMeta.test.ts``
Adds session model-mode persistence helpers, NewSessionDraft fields (selectedProfileId, modelMode), storage.replaceSettings API, expanded storage merging including modelMode, and buildOutgoingMessageMeta helper + tests.
Logging, dev-guards & misc tests/utilities
\sources/hooks/useCLIDetection.ts`, `sources/realtime/*.tsx`, `sources/hooks/useEnvironmentVariables.test.ts`, `sources/components/CommandPalette/CommandPaletteProvider.tsx`, `sources/utils/ignoreNextRowPress.ts`, `sources/utils/promptUnsavedChangesAlert.ts`, `sources/profileRouteParams.ts`, tests...
Convert console logs to DEV guards, add tests for env resolution/persistence, small modal typing tightening, and utilities/tests (ignoreNextRowPress, promptUnsavedChangesAlert, profile param helpers).
Translations & text reorganization
\sources/text/_default.ts`(removed),`sources/text/_types.ts`, `sources/text/index.ts`, `sources/text/translations/en.ts`, `sources/text/translations/{ca,es,it,ja,pl,pt,ru,zh-Hans}.ts``
Move English translations to translations/en.ts, centralize TranslationStructure in _types.ts, update index imports, and expand many locale files with profile/env-var/Gemini keys.
Styles / web modal tweaks
\sources/theme.css`, `sources/modal/components/BaseModal.tsx`, `sources/modal/components/WebAlertModal.tsx``
Add responsive web modal sizing, pointer-events/web event propagation guards, and WebAlertModal layout refinements.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User as User (UI)
participant AgentInput as AgentInput (component)
participant Storage as Storage
participant Sync as Sync/Network
participant API as External API
User->>AgentInput: compose message + choose modelMode/permissionMode
AgentInput->>Storage: persist session modelMode via updateSessionModelMode
AgentInput->>Sync: send message payload (buildOutgoingMessageMeta includes model, permission)
Sync->>Storage: read session metadata (profile, modelMode) during send
Sync->>API: post message with meta and model
API-->>Sync: response/events (task_started, task_complete, tool results)
Sync->>Storage: apply updates to session state and thinking flag

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇
I hopped through code, a curious hare,
Profiles sprouted everywhere.
Gemini learned which mode to wear,
Pickers polished with tender care.
New paths, less wizardry—hooray! I’ll share.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main changes: stabilizing the new-session UX, implementing profiles gating, and improving environment variable safety. It directly relates to the core objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @leeroybrun, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on stabilizing and refining the new session creation and profile management features introduced in a previous large merge. It addresses critical regressions and safety concerns, enhances user experience by making new functionalities optional and coherent, and improves the underlying architecture for better maintainability and security. The changes aim to restore a familiar workflow while integrating powerful new capabilities in a controlled and robust manner.

Highlights

  • New Session UX Restoration: The 'New Session' experience has been reverted to its pre-#272 modal and prompt-first workflow, while retaining the new wizard and profiles as opt-in features.
  • Optional and Coherent Profiles/Env Features: New features introduced in PR272, such as profiles and environment variable management, are now optional and more coherently integrated. Users can independently enable profiles with the standard new session screen or the new wizard.
  • Critical Bug Fixes and Safety Improvements: Several non-UI regressions and safety issues from PR272 have been addressed, including fixes for tool result parsing, settings parsing fragility, logging of sensitive data, and environment variable handling. Profile editing is now unmount-safe, and invalid profiles are prevented from being persisted.
  • UI Component Reusability and Wizard Enhancements: Existing UI primitives are now reused across the application to reduce duplication. The session wizard has been improved with better search and favorites display, a new AI Backend step, and correct Codex profile enablement.
  • Enhanced Profile Management: Profiles now allow selection of the AI Backend directly within profile settings. Profiles are also integrated into the standard 'new session' modal with a chip and picker, and a modal for environment variable preview.
  • Internationalization (i18n) Structure Cleanup: The i18n structure has been refined by separating translation types from translation content, improving maintainability and clarity.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an excellent and comprehensive pull request that successfully stabilizes the new-session UX, gates new features behind settings, and hardens the logic for profiles and environment variables. The restoration of the pre-existing "New Session" modal flow while making the new wizard an opt-in feature is a great improvement for user experience. The extensive refactoring to introduce reusable components like MachineSelector, PathSelector, and ItemRowActions significantly improves code maintainability and consistency across the application.

Key improvements I noted include:

  • Robustness: The settings parser is now fault-tolerant, preventing a single invalid profile from resetting all user settings. Environment variable parsing has been made much more reliable, correctly distinguishing between unset and empty variables. Message normalization for tool results is also more robust, preventing messages from being dropped.
  • Security: The implementation now includes heuristics to avoid fetching secret-like environment variables from remote machines, which is a crucial security enhancement. Logging of potentially sensitive data has also been commendably reduced.
  • State Management: The navigation and state management for profile editing have been completely overhauled, moving away from a fragile callback-based system to a much more robust ID-based routing approach that correctly handles unsaved changes.

Overall, this is a very high-quality contribution that addresses the stated goals effectively and substantially improves the stability and maintainability of the codebase. I found no issues that meet the requested severity threshold for comments.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
sources/modal/components/WebAlertModal.tsx (1)

6-6: Use StyleSheet from react-native-unistyles instead of react-native.

Per coding guidelines, all styling should use StyleSheet.create from react-native-unistyles. This enables theme and runtime access in style definitions.

♻️ Suggested fix
-import { StyleSheet } from 'react-native';
+import { StyleSheet } from 'react-native-unistyles';
sources/app/(app)/new/pick/machine.tsx (1)

11-28: Move the stylesheet to the end of the file.

App pages should place styles at the very end of the file; please relocate the StyleSheet.create(...) block accordingly. As per coding guidelines.

sources/sync/storage.ts (1)

808-826: Add persistence for modelMode like permissionMode.

The applySessions handler loads sessionPermissionModes on app startup (line 305-306) but does not load modelMode. Unlike permissionMode, which is explicitly saved via saveSessionPermissionModes and restored during sync, modelMode updates are lost on app restart. Implement saveSessionModelModes and load it in applySessions to match the permissionMode persistence pattern, or remove the load-restoration logic if modelMode should not be persisted.

🤖 Fix all issues with AI agents
In `@sources/app/`(app)/new/pick/profile.tsx:
- Around line 146-351: Multiple inline style objects (e.g., container styles in
renderProfileRowRightElement, renderDefaultEnvironmentRowRightElement, the
ItemList paddingTop, and Item rightElement row wrappers) violate the guideline;
extract them into a Unistyles stylesheet created via StyleSheet.create((theme)
=> ({})) placed at the end of the file and replace the inline style objects with
the corresponding style names. Specifically, replace the View style={{
flexDirection: 'row', alignItems: 'center', gap: 16 }}, the inner View
width/align/justify styles, Ionicons style opacity, the ItemList style={{
paddingTop: 0 }}, and any other inline style occurrences used in Item, ItemGroup
or ItemRowActions with styles from the new stylesheet (e.g., row,
rowIconWrapper, checkIcon) and import/use the theme parameter in the stylesheet
for colors.

In `@sources/components/AgentInput.tsx`:
- Around line 965-972: The Text label in AgentInput.tsx currently renders a
user-visible string without localization and hides a zero count by using a
truthy check; update the rendering that uses props.envVarsCount (the Text
element showing "Env Vars" / `Env Vars (${props.envVarsCount})`) to call the
translation function t(...) for the base label and use an explicit numeric
check/format (e.g., always render the count with props.envVarsCount !==
undefined ? props.envVarsCount : '' or use a pluralized/localized format via t
with a count) so that zero displays as "Env Vars (0)" and the text is localized;
adjust the Text content to use the translated string returned by t(...) and
include the numeric count in the localized output.

In `@sources/components/EnvironmentVariableCard.tsx`:
- Line 103: The fallbackOperator state is only initialized once from
parsed.fallbackOperator and can become stale if variable.value (or parsed)
changes; update the component to derive fallbackOperator from props instead of
fixed state or keep it stateful but sync it when parsed/variable.value changes:
replace the useState<[fallbackOperator]> initialization with a derived value
(e.g., compute fallbackOperator = parsed.fallbackOperator) or add a useEffect
that updates the fallbackOperator state when parsed or variable.value changes;
reference the existing fallbackOperator variable, parsed.fallbackOperator, and
any related hooks like useRemoteVariable/remoteVariableName/defaultValue to
ensure all dependent state stays in sync.
- Around line 112-120: The effect in EnvironmentVariableCard uses onUpdate which
is currently passed as an inline arrow from the parent and thus changes every
render; to fix, memoize the callback in the parent (EnvironmentVariablesList)
with useCallback (e.g. memoize the function that calls handleUpdateVariable) or
change the parent to pass handleUpdateVariable directly via a curried function
so the reference is stable; ensure the prop name onUpdate kept on
EnvironmentVariableCard receives a stable function so the useEffect in
EnvironmentVariableCard (which references useRemoteVariable, remoteVariableName,
defaultValue, fallbackOperator, variable.value and onUpdate) no longer
retriggers unnecessarily.

In `@sources/components/ItemActionsMenuModal.tsx`:
- Around line 33-89: Move all inline style objects in ItemActionsMenuModal (the
outer container View, the header View, the title Text, the ScrollView and its
contentContainerStyle, and the Pressable style function) into a Unistyles
stylesheet using StyleSheet.create((theme) => ({})); create named entries like
container, header, title, scroll, scrollContent, and pressable and replace the
inline style props with styles.container, styles.header, styles.title,
style={styles.scroll} and contentContainerStyle={styles.scrollContent}; for the
Pressable, implement style={({pressed}) => ({...styles.pressable, opacity:
pressed ? 0.7 : 1})} so pressed state is preserved; keep icon props and mapping
logic (props.title, props.actions, closeThen) unchanged.
- Around line 1-70: The "Actions" header in ItemActionsMenuModal is hardcoded;
update the ItemGroup title prop to use the i18n translator (e.g.
t('itemActions.actions') or an appropriate key) instead of the literal string,
and ensure the translation key is added to the locale files; locate the title
usage inside the ItemActionsMenuModal component where ItemGroup is rendered and
replace "Actions" with t(...) while importing/using the same t function or hook
your project uses for translations.

In `@sources/components/newSession/EnvironmentVariablesPreviewModal.tsx`:
- Around line 75-246: Extract all inline style objects used in the
EnvironmentVariablesPreviewModal component (the top-level container View, the
header View, title Text, close Pressable style, ScrollView/content container,
the description Text block, the empty-state View/Text, and any per-ItemGroup
wrapper) into a Unistyles stylesheet created with StyleSheet.create((theme) =>
({})) and replace inline style props with styles.* references; keep using the
existing theme and Platform checks (e.g., the top-level style object, header
block, the description Text that uses Platform.select, and the empty message
block), move complex shared typography merges into the stylesheet but preserve
dynamic bits (like pressed opacity on Pressable and the web onWheel spread)
inline as needed, and update imports/usage so all style lookups reference the
new styles object.

In `@sources/components/newSession/MachineSelector.tsx`:
- Around line 81-86: The current filterItem function in MachineSelector.tsx only
searches displayName and host from machine.metadata, so if metadata is null the
displayed machine id won't match; update filterItem to also normalize and search
machine.id (e.g., const id = (machine.id || '').toLowerCase()) and include
id.includes(search) in the return condition alongside
displayName.includes(search) || host.includes(search).

In `@sources/components/newSession/PathSelector.tsx`:
- Around line 317-574: The component uses hard-coded user strings (e.g., "Search
paths...", ItemGroup titles like "Enter Path", "Recent Paths", "Favorite Paths",
"Suggested Paths", "Paths", placeholders such as "Enter path (e.g.
/home/user/projects)", and empty-state labels like "No matches", "No recent
paths", "No favorite paths", "No suggested paths", "No paths") — update each
visible string to use the translation helper t(...) from '@/text' instead of
literals; search for usages around SearchHeader, ItemGroup, the TextInput
placeholder in PathSelector, and Item title props as well as any calls to
renderRightElement/renderCustomRightElement and setPathAndFocus to ensure all
displayed text is wrapped (e.g., t('pathSelector.searchPlaceholder'),
t('pathSelector.enterPathTitle'), etc.), keeping the same string keys across
similar occurrences and passing translated strings into the existing props
(value/placeholder/title).
- Around line 70-73: Detect and prevent mixed controlled/uncontrolled usage for
the search input by adding an explicit isSearchQueryControlled flag (derived
from controlledSearchQuery and onChangeSearchQueryProp) and assert or throw when
only one of controlledSearchQuery/onChangeSearchQueryProp is provided; use
useCallback to create setSearchQuery that either calls onChangeSearchQueryProp
or setUncontrolledSearchQuery depending on isSearchQueryControlled, and keep
searchQuery sourced from controlledSearchQuery when controlled or
uncontrolledSearchQuery otherwise (update symbols: uncontrolledSearchQuery,
setUncontrolledSearchQuery, controlledSearchQuery, onChangeSearchQueryProp,
searchQuery, setSearchQuery, isSearchQueryControlled, useCallback). Also replace
all hardcoded UI strings ("Search paths...", "No matches", "No recent paths",
"No favorite paths", "No suggested paths", "No paths") with t(...) calls for
i18n and move any inline containerStyle objects passed to SearchHeader into the
Unistyles stylesheet (hoist styles and reference them instead).

In `@sources/components/Switch.web.tsx`:
- Around line 50-58: The thumb View always uses the active color; update the
style in the Switch component so backgroundColor uses the switch state (value) —
i.e., set backgroundColor to value ? theme.colors.switch.thumb.active :
theme.colors.switch.thumb.inactive (keeping styles.thumb and transform: [{
translateX }] intact) so thumb color matches the track logic.

In `@sources/modal/components/CustomModal.tsx`:
- Around line 41-43: The modal is wired to close directly via the raw onClose
prop, bypassing the animation-aware handler; change the CommandPaletteModal prop
from onClose={onClose} to onClose={handleClose} so that all close triggers
(overlay/ESC/button) go through the animation-aware handler (see
CommandPaletteModal, CommandPalette, handleClose and isClosing) and preserve the
existing delayed close animation behavior.

In `@sources/sync/profileGrouping.ts`:
- Around line 44-46: favoriteIds is currently built directly from
favoriteProfileIds which may include IDs that don't resolve to actual profiles,
causing inconsistency with favoriteProfiles (which filters via isProfile);
change the logic so favoriteProfiles is created first using
favoriteProfileIds.map(...).filter(isProfile) (using customById and
getBuiltInProfile) and then derive favoriteIds from that filtered list (e.g.,
new Set(favoriteProfiles.map(p => p.id))) so both collections are consistent and
only contain valid profile IDs.

In `@sources/sync/sync.ts`:
- Line 1175: The call to storage.getState().replaceSettings(mergedSettings,
data.currentVersion) unconditionally overwrites settings and can cause version
downgrades; replace it with storage.getState().applySettings(mergedSettings,
data.currentVersion) so the state only updates when the incoming version is
newer (use applySettings in both places where replaceSettings is currently used,
e.g., the two occurrences at sync logic points), preserving the
version-comparison safety implemented in storage.ts (applySettings vs
replaceSettings).
🧹 Nitpick comments (33)
sources/utils/promptUnsavedChangesAlert.test.ts (1)

5-25: Test coverage could be expanded.

The test correctly validates the 'save' path. Consider adding tests for the 'discard' and 'keepEditing' resolution paths to ensure complete coverage of all three decision outcomes.

💡 Additional test cases
it('resolves to discard when the Discard button is pressed', async () => {
    let lastButtons: AlertButton[] | undefined;

    const alert = (_title: string, _message?: string, buttons?: AlertButton[]) => {
        lastButtons = buttons;
    };

    const promise = promptUnsavedChangesAlert(alert, {
        title: 'Discard changes',
        message: 'You have unsaved changes.',
        discardText: 'Discard',
        saveText: 'Save',
        keepEditingText: 'Keep editing',
    });

    lastButtons?.find((b) => b.text === 'Discard')?.onPress?.();

    await expect(promise).resolves.toBe('discard');
});

it('resolves to keepEditing when the Keep editing button is pressed', async () => {
    let lastButtons: AlertButton[] | undefined;

    const alert = (_title: string, _message?: string, buttons?: AlertButton[]) => {
        lastButtons = buttons;
    };

    const promise = promptUnsavedChangesAlert(alert, {
        title: 'Discard changes',
        message: 'You have unsaved changes.',
        discardText: 'Discard',
        saveText: 'Save',
        keepEditingText: 'Keep editing',
    });

    lastButtons?.find((b) => b.text === 'Keep editing')?.onPress?.();

    await expect(promise).resolves.toBe('keepEditing');
});
sources/components/tools/views/GeminiEditView.tsx (1)

62-73: Unnecessary React Fragment wrapper.

The Fragment (<>...</>) wraps only a single ToolSectionView child. It can be removed for cleaner JSX.

Proposed simplification
     return (
-        <>
-            <ToolSectionView fullWidth>
-                <ToolDiffView 
-                    oldText={oldString} 
-                    newText={newString} 
-                    showLineNumbers={showLineNumbersInToolViews}
-                    showPlusMinusSymbols={showLineNumbersInToolViews}
-                />
-            </ToolSectionView>
-        </>
+        <ToolSectionView fullWidth>
+            <ToolDiffView 
+                oldText={oldString} 
+                newText={newString} 
+                showLineNumbers={showLineNumbersInToolViews}
+                showPlusMinusSymbols={showLineNumbersInToolViews}
+            />
+        </ToolSectionView>
     );
sources/components/tools/knownTools.tsx (3)

543-566: Consider extracting shared reasoning tool logic.

GeminiReasoning is nearly identical to CodexReasoning (lines 519-542), differing only in the status enum values ('canceled' vs 'error'). Consider extracting a factory function or shared base definition to reduce duplication.


657-686: Duplicated parsing logic with GeminiExecuteView.tsx.

The title parsing logic in execute.title and execute.extractSubtitle (extracting command before [, description from parentheses) duplicates the extractExecuteInfo function in GeminiExecuteView.tsx (lines 12-44). Consider extracting a shared utility.


763-817: Significant duplication with CodexPatch.

GeminiPatch shares nearly identical extractSubtitle and extractDescription logic with CodexPatch (lines 687-740). The main difference is isMutable: true. Consider extracting shared helpers or a factory function.

sources/utils/ignoreNextRowPress.test.ts (1)

4-16: Use try/finally (or afterEach) to restore timers.
Ensures real timers are restored even if an assertion fails.

♻️ Suggested tweak
 it('resets the ignore flag on the next tick', () => {
     vi.useFakeTimers();
-    const ref = { current: false };
-
-    ignoreNextRowPress(ref);
-    expect(ref.current).toBe(true);
-
-    vi.runAllTimers();
-    expect(ref.current).toBe(false);
-
-    vi.useRealTimers();
+    try {
+        const ref = { current: false };
+        ignoreNextRowPress(ref);
+        expect(ref.current).toBe(true);
+        vi.runAllTimers();
+        expect(ref.current).toBe(false);
+    } finally {
+        vi.useRealTimers();
+    }
 });
sources/sync/persistence.ts (1)

152-154: Consider validating modelMode against known values.

The current implementation accepts any string as a valid ModelMode. If corrupted or outdated data contains an invalid mode (e.g., a removed model identifier), it will pass through unchecked and could cause issues downstream.

The same pattern exists for permissionMode (lines 149-151), so this is a pre-existing concern. Consider adding a validation helper that checks against known values and falls back to 'default' if invalid.

Example validation approach
const VALID_MODEL_MODES: ModelMode[] = [
    'default', 'adaptiveUsage', 'sonnet', 'opus',
    'gpt-5-codex-high', 'gpt-5-codex-medium', 'gpt-5-codex-low',
    'gpt-5-minimal', 'gpt-5-low', 'gpt-5-medium', 'gpt-5-high',
    'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'
];

const modelMode: ModelMode = VALID_MODEL_MODES.includes(parsed.modelMode)
    ? parsed.modelMode
    : 'default';
sources/modal/components/BaseModal.tsx (1)

7-7: Consider using StyleSheet.create from react-native-unistyles.

The file imports StyleSheet from react-native, but per coding guidelines, StyleSheet.create should be imported from 'react-native-unistyles' for all styling. This would provide consistent theming capabilities.

Suggested import change
 import {
     View,
     Modal,
     TouchableWithoutFeedback,
     Animated,
-    StyleSheet,
     KeyboardAvoidingView,
     Platform
 } from 'react-native';
+import { StyleSheet } from 'react-native-unistyles';

Based on coding guidelines: "Use StyleSheet.create from 'react-native-unistyles' for all styling".

sources/sync/permissionMapping.ts (1)

3-3: Avoid duplicating AgentType.

Line 3 redefines AgentType, which already exists in sources/sync/modelOptions.ts. Importing it prevents drift if the agent list changes later.

♻️ Proposed refactor
-import type { PermissionMode } from './permissionTypes';
-
-type AgentType = 'claude' | 'codex' | 'gemini';
+import type { PermissionMode } from './permissionTypes';
+import type { AgentType } from './modelOptions';
sources/components/ItemGroup.tsx (1)

13-15: Prefer the @/ alias for new imports/exports.

Switching to @/components/ItemGroup.dividers keeps new references aligned with the path-alias guideline. As per coding guidelines, ...

♻️ Proposed refactor
-import { withItemGroupDividers } from './ItemGroup.dividers';
-
-export { withItemGroupDividers } from './ItemGroup.dividers';
+import { withItemGroupDividers } from '@/components/ItemGroup.dividers';
+
+export { withItemGroupDividers } from '@/components/ItemGroup.dividers';
sources/components/Item.tsx (2)

115-116: Inconsistent indentation detected.

Lines 116, 202-205, and 291-307 use tab characters instead of the required 4 spaces for indentation. As per coding guidelines, use 4 spaces for indentation consistently.

🔧 Suggested fix
     const { theme } = useUnistyles();
     const styles = stylesheet;
-	    const selectionContext = React.useContext(ItemGroupSelectionContext);
-	    
+    const selectionContext = React.useContext(ItemGroupSelectionContext);
+    
     // Platform-specific measurements

291-307: Indentation issues in Pressable block.

The tab-based indentation continues here. Consider normalizing to 4 spaces for consistency with the codebase conventions.

🔧 Suggested fix for indentation
-	    if (isInteractive) {
-	        return (
-	            <Pressable
+    if (isInteractive) {
+        return (
+            <Pressable
                 onPress={handlePress}
                 onLongPress={onLongPress}
                 onPressIn={handlePressIn}
                 onPressOut={handlePressOut}
-	                disabled={disabled || loading}
-	                style={({ pressed }) => [
-	                    {
-	                        backgroundColor: pressed && isIOS && !isWeb
-	                            ? theme.colors.surfacePressedOverlay
-	                            : (showSelectedBackground ? theme.colors.surfaceSelected : 'transparent'),
-	                        opacity: disabled ? 0.5 : 1
-	                    },
-	                    pressableStyle
-	                ]}
+                disabled={disabled || loading}
+                style={({ pressed }) => [
+                    {
+                        backgroundColor: pressed && isIOS && !isWeb
+                            ? theme.colors.surfacePressedOverlay
+                            : (showSelectedBackground ? theme.colors.surfaceSelected : 'transparent'),
+                        opacity: disabled ? 0.5 : 1
+                    },
+                    pressableStyle
+                ]}
sources/components/newSession/ProfileCompatibilityIcon.tsx (1)

31-34: Clarify the purpose of the unused useUnistyles() call.

The useUnistyles() hook is called but its return value is not used. If this is intentional for subscribing to theme updates (to trigger re-renders), consider adding a brief comment to clarify this intent for future maintainers.

 export function ProfileCompatibilityIcon({ profile, size = 32, style }: Props) {
-    useUnistyles();
+    useUnistyles(); // Subscribe to theme changes for re-render
     const styles = stylesheet;
sources/profileRouteParams.test.ts (1)

4-39: Good test coverage for consumeProfileIdParam.

The tests comprehensively cover the main use cases:

  • Missing param
  • Empty string (deselection)
  • Matching current selection
  • Different selection
  • Array input

Consider adding a test for the empty array edge case to ensure consistent behavior:

📝 Optional: Add empty array test
    it('treats empty array as missing param', () => {
        expect(consumeProfileIdParam({ profileIdParam: [], selectedProfileId: 'abc' })).toEqual({
            nextSelectedProfileId: undefined,
            shouldClearParam: false,
        });
    });
sources/sync/modelOptions.ts (1)

11-15: User-visible strings should use t(...) for internationalization.

Per coding guidelines, all user-visible strings must use the t(...) function from @/text for internationalization. The labels and descriptions here are displayed in the UI.

♻️ Suggested refactor
+import { t } from '@/text';
+
 const GEMINI_MODEL_OPTIONS: readonly ModelOption[] = [
-    { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', description: 'Most capable' },
-    { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', description: 'Fast & efficient' },
-    { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite', description: 'Fastest' },
+    { value: 'gemini-2.5-pro', label: t('gemini_model_pro'), description: t('gemini_model_pro_desc') },
+    { value: 'gemini-2.5-flash', label: t('gemini_model_flash'), description: t('gemini_model_flash_desc') },
+    { value: 'gemini-2.5-flash-lite', label: t('gemini_model_flash_lite'), description: t('gemini_model_flash_lite_desc') },
 ];

Note: You'll need to add the corresponding translation keys to your i18n files.

sources/modal/components/WebAlertModal.tsx (2)

37-114: StyleSheet created inside component causes unnecessary re-creation on every render.

Move the stylesheet outside the component using unistyles' function mode to access theme dynamically. This improves performance by avoiding repeated style object creation.

♻️ Suggested approach
// Move outside component, using function mode for theme access
const stylesheet = StyleSheet.create((theme) => ({
    container: {
        backgroundColor: theme.colors.surface,
        // ... rest of styles
    },
    // ... other styles
}));

// Inside component:
const { styles } = useStyles(stylesheet);

28-33: User-visible strings should use t(...) for internationalization.

Per coding guidelines, 'Cancel' and 'OK' should use the translation function.

♻️ Suggested fix
+import { t } from '@/text';
+
 const buttons = isConfirm
     ? [
-        { text: config.cancelText || 'Cancel', style: 'cancel' as const },
-        { text: config.confirmText || 'OK', style: config.destructive ? 'destructive' as const : 'default' as const }
+        { text: config.cancelText || t('cancel'), style: 'cancel' as const },
+        { text: config.confirmText || t('ok'), style: config.destructive ? 'destructive' as const : 'default' as const }
     ]
     : config.buttons || [{ text: 'OK', style: 'default' as const }];
sources/modal/ModalManager.ts (1)

98-114: Acceptable type casting for generic-to-concrete bridging.

The as unknown as casts on lines 109-110 are necessary because the generic P is erased when storing in the non-generic showModalFn. The approach is type-safe given the constraint P extends CustomModalInjectedProps.

However, consider adding a brief comment explaining why the casts are needed:

📝 Suggested comment
         const modalConfig: Omit<CustomModalConfig, 'id'> = {
             type: 'custom',
+            // Casts required: generic P erased when stored in non-generic modal state
             component: config.component as unknown as CustomModalConfig['component'],
             props: config.props as unknown as CustomModalConfig['props'],
         };
sources/sync/typesRaw.ts (1)

421-445: Robust content normalization helper.

The toolResultContentToText function handles the various content formats well:

  • Null/undefined → empty string
  • String → passthrough
  • Claude text block arrays → concatenation
  • Other arrays/objects → JSON stringification with fallback

One minor consideration: the function is defined inside normalizeRawMessage, which means it's recreated on every call. For performance, consider hoisting it outside the function.

♻️ Optional: Hoist helper for performance
+const toolResultContentToText = (content: unknown): string => {
+    if (content === null || content === undefined) return '';
+    if (typeof content === 'string') return content;
+    if (Array.isArray(content)) {
+        const maybeTextBlocks = content as Array<{ type?: unknown; text?: unknown }>;
+        const isTextBlocks = maybeTextBlocks.every((b) => b && typeof b === 'object' && b.type === 'text' && typeof b.text === 'string');
+        if (isTextBlocks) {
+            return maybeTextBlocks.map((b) => b.text as string).join('');
+        }
+        try { return JSON.stringify(content); } catch { return String(content); }
+    }
+    try { return JSON.stringify(content); } catch { return String(content); }
+};
+
 export function normalizeRawMessage(id: string, localId: string | null, createdAt: number, raw: RawRecord): NormalizedMessage | null {
     // ... remove inner definition ...
sources/sync/typesRaw.spec.ts (1)

1493-1545: Good test coverage for ACP tool result normalization.

The tests verify that both tool-result and tool-call-result ACP messages correctly normalize text block arrays to plain strings. This aligns with the toolResultContentToText implementation in typesRaw.ts.

Consider adding edge case tests for additional coverage:

📝 Suggested additional test cases
it('normalizes ACP tool-result with string output', () => {
    const raw = {
        role: 'agent' as const,
        content: {
            type: 'acp' as const,
            provider: 'gemini' as const,
            data: {
                type: 'tool-result' as const,
                callId: 'call_string',
                output: 'plain string output',
                id: 'acp-msg-string',
            },
        },
    };
    const normalized = normalizeRawMessage('msg-3', null, Date.now(), raw);
    expect(normalized?.role).toBe('agent');
    if (normalized && normalized.role === 'agent') {
        const item = normalized.content[0];
        if (item.type === 'tool-result') {
            expect(item.content).toBe('plain string output');
        }
    }
});

it('normalizes ACP tool-result with JSON object output', () => {
    const raw = {
        role: 'agent' as const,
        content: {
            type: 'acp' as const,
            provider: 'codex' as const,
            data: {
                type: 'tool-result' as const,
                callId: 'call_json',
                output: { status: 'success', data: [1, 2, 3] },
                id: 'acp-msg-json',
            },
        },
    };
    const normalized = normalizeRawMessage('msg-4', null, Date.now(), raw);
    expect(normalized?.role).toBe('agent');
    if (normalized && normalized.role === 'agent') {
        const item = normalized.content[0];
        if (item.type === 'tool-result') {
            expect(item.content).toBe('{"status":"success","data":[1,2,3]}');
        }
    }
});
sources/components/ProfileEditForm.tsx (2)

57-113: MachinePreviewModal uses inline styles instead of StyleSheet.

Per coding guidelines, styling should use StyleSheet.create from 'react-native-unistyles'. The MachinePreviewModal component uses extensive inline styles which could be extracted to the stylesheet at the bottom of the file.

♻️ Suggested refactor to use StyleSheet
 function MachinePreviewModal(props: MachinePreviewModalProps) {
     const { theme } = useUnistyles();
     const { height: windowHeight } = useWindowDimensions();
+    const styles = stylesheet;
     // ... existing logic ...
     const maxHeight = Math.min(720, Math.max(420, Math.floor(windowHeight * 0.85)));

     return (
-        <View style={{
-            width: '92%',
-            maxWidth: 560,
-            height: maxHeight,
-            maxHeight,
-            backgroundColor: theme.colors.groupped.background,
-            borderRadius: 16,
-            overflow: 'hidden',
-            borderWidth: 1,
-            borderColor: theme.colors.divider,
-            flexShrink: 1,
-        }}>
+        <View style={[styles.modalContainer, { height: maxHeight, maxHeight }]}>
             {/* ... header and content ... */}
         </View>
     );
 }

// Add to stylesheet:
+    modalContainer: {
+        width: '92%',
+        maxWidth: 560,
+        backgroundColor: theme.colors.groupped.background,
+        borderRadius: 16,
+        overflow: 'hidden',
+        borderWidth: 1,
+        borderColor: theme.colors.divider,
+        flexShrink: 1,
+    },
+    modalHeader: {
+        paddingHorizontal: 16,
+        paddingVertical: 12,
+        flexDirection: 'row',
+        alignItems: 'center',
+        justifyContent: 'space-between',
+        borderBottomWidth: 1,
+        borderBottomColor: theme.colors.divider,
+    },
+    modalTitle: {
+        fontSize: 17,
+        color: theme.colors.text,
+        ...Typography.default('semiBold'),
+    },

Based on coding guidelines, use StyleSheet.create from 'react-native-unistyles' for all styling.


466-501: Button container uses inline styles - consider extracting to stylesheet.

Similar to the modal, the button container and button styles are inline. While functional, extracting to the stylesheet would be more consistent with the codebase patterns.

♻️ Optional: Extract button styles
-            <View style={{ paddingHorizontal: Platform.select({ ios: 16, default: 12 }), paddingTop: 12 }}>
-                <View style={{ flexDirection: 'row', gap: 12 }}>
+            <View style={styles.buttonContainer}>
+                <View style={styles.buttonRow}>

// Add to stylesheet:
+    buttonContainer: {
+        paddingHorizontal: Platform.select({ ios: 16, default: 12 }),
+        paddingTop: 12,
+    },
+    buttonRow: {
+        flexDirection: 'row',
+        gap: 12,
+    },
sources/text/translations/zh-Hans.ts (2)

15-17: Consider removing unused plural helper.

Chinese (Simplified) doesn't have grammatical plural forms like English. The plural helper function is defined but appears unused in the translation strings (Chinese typically uses the same form regardless of count). Consider removing this unused function to keep the file clean.

♻️ Suggested removal
-/**
- * Chinese plural helper function
- * `@param` options - Object containing count, singular, and plural forms
- * `@returns` The appropriate form based on count
- */
-function plural({ count, singular, plural }: { count: number; singular: string; plural: string }): string {
-    return count === 1 ? singular : plural;
-}

509-509: Plural helper used but may not be linguistically appropriate.

The plural helper is used here: ${plural({ count, singular: '工具', plural: '工具' })} — but both singular and plural are identical ('工具'), confirming Chinese doesn't distinguish. This works but is redundant.

sources/components/ItemRowActions.tsx (1)

54-57: Consider using proper event types instead of any.

The (e: any) type annotation bypasses TypeScript's type safety. While the optional chaining on stopPropagation handles cross-platform scenarios, you could use a more specific type.

Suggested improvement
-                    onPress={(e: any) => {
-                        e?.stopPropagation?.();
+                    onPress={(e) => {
+                        (e as any)?.stopPropagation?.();
                         action.onPress();
                     }}

Alternatively, import GestureResponderEvent from react-native and use that type, though the optional chaining pattern suggests cross-platform event differences that may warrant the current approach.

Also applies to: 71-74

sources/components/SearchHeader.tsx (1)

105-113: Consider improving touch target for the clear button.

The clear icon uses Ionicons's onPress directly. While functional, wrapping it in a Pressable with hitSlop would provide a larger, more accessible touch target, consistent with how other action icons are handled in ItemRowActions.tsx (lines 50-64).

Suggested improvement
                     {value.length > 0 && (
-                        <Ionicons
-                            name="close-circle"
-                            size={20}
-                            color={theme.colors.textSecondary}
-                            onPress={() => onChangeText('')}
-                            style={styles.clearIcon}
-                        />
+                        <Pressable
+                            onPress={() => onChangeText('')}
+                            hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+                            style={styles.clearIcon}
+                        >
+                            <Ionicons
+                                name="close-circle"
+                                size={20}
+                                color={theme.colors.textSecondary}
+                            />
+                        </Pressable>
                     )}

This would require adding Pressable to the imports from react-native.

sources/app/(app)/new/pick/path.tsx (1)

91-94: Consider adding proper type for session instead of casting to any.

The as any cast on Line 93 loses type safety. If Session type is available from storage types, use it directly or create a type guard.

Suggested improvement
-                const session = item as any;
-                if (session.metadata?.machineId === params.machineId && session.metadata?.path) {
+                if (typeof item === 'object' && 'metadata' in item) {
+                    const session = item as Session;
+                    if (session.metadata?.machineId === params.machineId && session.metadata?.path) {
sources/components/EnvironmentVariablesList.tsx (1)

180-181: Using envVar.name as key may cause issues if names can be edited.

If users can rename environment variables, using name as the React key could cause unexpected behavior. Consider using a stable identifier or index-based key with proper handling.

Consider using index or stable ID
-                            <EnvironmentVariableCard
-                                key={envVar.name}
+                            <EnvironmentVariableCard
+                                key={`${index}-${envVar.name}`}
sources/app/(app)/new/pick/profile-edit.tsx (1)

157-161: Consider wrapping router.replace in try-catch for navigation safety.

If router.replace throws (e.g., invalid route), the user would be stuck. While unlikely, defensive error handling could improve robustness.

sources/app/(app)/settings/profiles.tsx (1)

233-276: Consider extracting a reusable profile row renderer to reduce duplication.

The three profile group sections (favorites, custom, built-in) share nearly identical rendering logic with only minor differences (e.g., whether onDelete is passed to actions). A shared renderProfileItem helper would reduce ~120 lines of duplication.

♻️ Example extraction
const renderProfileItem = React.useCallback((
    profile: AIBackendProfile,
    options: { showDelete?: boolean }
) => {
    const isSelected = selectedProfileId === profile.id;
    const isFavorite = favoriteProfileIdSet.has(profile.id);
    const actions = buildProfileActions({
        profile,
        isFavorite,
        favoriteActionColor: selectedIndicatorColor,
        nonFavoriteActionColor: theme.colors.textSecondary,
        onToggleFavorite: () => toggleFavoriteProfile(profile.id),
        onEdit: () => handleEditProfile(profile),
        onDuplicate: () => handleDuplicateProfile(profile),
        ...(options.showDelete && { onDelete: () => { void handleDeleteProfile(profile); } }),
    });
    
    return (
        <Item
            key={profile.id}
            title={profile.name}
            subtitle={getProfileBackendSubtitle(profile)}
            leftElement={<ProfileCompatibilityIcon profile={profile} />}
            onPress={() => handleSelectProfile(profile.id)}
            showChevron={false}
            selected={isSelected}
            rightElement={/* ... */}
        />
    );
}, [/* deps */]);

Also applies to: 281-324, 328-370

sources/components/SearchableListSelector.tsx (1)

363-364: Consider making the fallback title generation more robust.

The fallback config.allSectionTitle ?? config.recentSectionTitle.replace('Recent ', 'All ') assumes the recent section title contains "Recent " which may not hold for all locales.

♻️ Suggested improvement
- <ItemGroup title={config.allSectionTitle ?? config.recentSectionTitle.replace('Recent ', 'All ')}>
+ <ItemGroup title={config.allSectionTitle ?? t('common.allItems')}>

Alternatively, make allSectionTitle required in the config when showAll is true.

sources/components/EnvironmentVariableCard.tsx (1)

71-96: Consider moving platform-specific styles to StyleSheet.create.

Per coding guidelines, styles should use StyleSheet.create from 'react-native-unistyles'. The useMemo approach works but doesn't leverage unistyles' optimization.

sources/components/newSession/PathSelector.tsx (1)

228-296: Move repeated inline styles into the Unistyles stylesheet.

Right-element containers and icon wrappers are styled inline; this conflicts with the “StyleSheet.create for all styling” guideline and recreates objects each render. Consider hoisting these styles into the stylesheet (and apply the same approach to other inline style objects in this file).

♻️ Example refactor
-const stylesheet = StyleSheet.create((theme) => ({
+const ITEM_RIGHT_GAP = 16;
+
+const stylesheet = StyleSheet.create((theme) => ({
     pathInputContainer: {
         flexDirection: 'row',
         alignItems: 'center',
         gap: 8,
         paddingHorizontal: 16,
         paddingVertical: 16,
     },
+    itemRightContainer: {
+        flexDirection: 'row',
+        alignItems: 'center',
+        gap: ITEM_RIGHT_GAP,
+    },
+    itemRightIconWrapper: {
+        width: 24,
+        alignItems: 'center',
+        justifyContent: 'center',
+    },
     pathInput: {
         flex: 1,
         backgroundColor: theme.colors.input.background,
         borderRadius: 10,
         paddingHorizontal: 12,
         minHeight: 36,
         position: 'relative',
         borderWidth: 0.5,
         borderColor: theme.colors.divider,
     },
 }));
-
-const ITEM_RIGHT_GAP = 16;
-            <View style={{ flexDirection: 'row', alignItems: 'center', gap: ITEM_RIGHT_GAP }}>
-                <View style={{ width: 24, alignItems: 'center', justifyContent: 'center' }}>
+            <View style={styles.itemRightContainer}>
+                <View style={styles.itemRightIconWrapper}>
...
-            <View style={{ flexDirection: 'row', alignItems: 'center', gap: ITEM_RIGHT_GAP }}>
-                <View style={{ width: 24, alignItems: 'center', justifyContent: 'center' }}>
+            <View style={styles.itemRightContainer}>
+                <View style={styles.itemRightIconWrapper}>

As per coding guidelines, please keep styling in StyleSheet.create (including other inline style objects in this file).

Comment on lines 112 to 120
React.useEffect(() => {
const newValue = useRemoteVariable && remoteVariableName.trim() !== ''
? `\${${remoteVariableName}${defaultValue ? `:-${defaultValue}` : ''}}`
? formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue, operator: fallbackOperator })
: defaultValue;

if (newValue !== variable.value) {
onUpdate(newValue);
}
}, [useRemoteVariable, remoteVariableName, defaultValue, variable.value, onUpdate]);
}, [useRemoteVariable, remoteVariableName, defaultValue, fallbackOperator, variable.value, onUpdate]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all usages of EnvironmentVariableCard component
rg -n 'EnvironmentVariableCard' --type tsx --type ts -B2 -A5

Repository: leeroybrun/happy

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Find all usages of EnvironmentVariableCard component without file type restrictions
rg -n 'EnvironmentVariableCard' -B2 -A5

Repository: leeroybrun/happy

Length of output: 3551


🏁 Script executed:

#!/bin/bash
# Read the EnvironmentVariablesList around the usage of EnvironmentVariableCard
sed -n '170,200p' sources/components/EnvironmentVariablesList.tsx

Repository: leeroybrun/happy

Length of output: 1704


🏁 Script executed:

#!/bin/bash
# Check the full component definition and handleUpdateVariable
sed -n '1,50p' sources/components/EnvironmentVariablesList.tsx

Repository: leeroybrun/happy

Length of output: 2135


🏁 Script executed:

#!/bin/bash
# Find handleUpdateVariable and other handlers in EnvironmentVariablesList
rg -n 'handleUpdateVariable|handleDeleteVariable|handleDuplicateVariable' sources/components/EnvironmentVariablesList.tsx -B2 -A3

Repository: leeroybrun/happy

Length of output: 1273


The effect dependency array is complete but includes a callback that changes on every render.

While the dependency array correctly includes onUpdate, it's defined as an inline arrow function on each parent render (onUpdate={(newValue) => handleUpdateVariable(index, newValue)}), causing the effect to run unnecessarily. Wrap the callback in useCallback in EnvironmentVariablesList to memoize it, or pass handleUpdateVariable directly with a curried approach to prevent re-running this effect on every parent render.

🤖 Prompt for AI Agents
In `@sources/components/EnvironmentVariableCard.tsx` around lines 112 - 120, The
effect in EnvironmentVariableCard uses onUpdate which is currently passed as an
inline arrow from the parent and thus changes every render; to fix, memoize the
callback in the parent (EnvironmentVariablesList) with useCallback (e.g. memoize
the function that calls handleUpdateVariable) or change the parent to pass
handleUpdateVariable directly via a curried function so the reference is stable;
ensure the prop name onUpdate kept on EnvironmentVariableCard receives a stable
function so the useEffect in EnvironmentVariableCard (which references
useRemoteVariable, remoteVariableName, defaultValue, fallbackOperator,
variable.value and onUpdate) no longer retriggers unnecessarily.

Comment on lines 50 to 58
<View
style={[
styles.thumb,
{
backgroundColor: theme.colors.switch.thumb.active,
transform: [{ translateX }],
},
]}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a switch.thumb.inactive color defined in the theme
rg -n "switch.*thumb" --type ts

Repository: leeroybrun/happy

Length of output: 375


🏁 Script executed:

# Find and examine the Switch.web.tsx file
fd "Switch.web.tsx" --exec cat -n {}

Repository: leeroybrun/happy

Length of output: 2360


🏁 Script executed:

# Also search for theme color definitions related to switch
rg -n "switch.*thumb" --type tsx

Repository: leeroybrun/happy

Length of output: 87


🏁 Script executed:

# Search for theme definitions related to switch colors
rg -n "switch" -A 5 -B 5 --type ts | head -100

Repository: leeroybrun/happy

Length of output: 5806


🏁 Script executed:

# Find theme configuration files
fd "theme" -o fd "*theme*" | grep -E "\.(ts|tsx|js|json)$"

Repository: leeroybrun/happy

Length of output: 183


🏁 Script executed:

# Search for where switch theme colors are defined
rg -n "thumb.*inactive" --type ts --type tsx

Repository: leeroybrun/happy

Length of output: 87


🏁 Script executed:

# Get the complete switch.thumb definitions from theme.ts
sed -n '75,85p' sources/theme.ts && echo "---DARK THEME---" && sed -n '280,290p' sources/theme.ts

Repository: leeroybrun/happy

Length of output: 693


🏁 Script executed:

# Search more broadly for all switch thumb color definitions
rg -n "thumb:" -A 2 sources/theme.ts

Repository: leeroybrun/happy

Length of output: 269


Use conditional color for thumb based on switch state.

Line 54 should use value ? theme.colors.switch.thumb.active : theme.colors.switch.thumb.inactive, matching the pattern used for track color on line 46. The theme defines both switch.thumb.active (#FFFFFF) and switch.thumb.inactive (#767577), but the component always uses the active color regardless of state.

Current code
                <View
                    style={[
                        styles.thumb,
                        {
                            backgroundColor: theme.colors.switch.thumb.active,
                            transform: [{ translateX }],
                        },
                    ]}
                />
🤖 Prompt for AI Agents
In `@sources/components/Switch.web.tsx` around lines 50 - 58, The thumb View
always uses the active color; update the style in the Switch component so
backgroundColor uses the switch state (value) — i.e., set backgroundColor to
value ? theme.colors.switch.thumb.active : theme.colors.switch.thumb.inactive
(keeping styles.thumb and transform: [{ translateX }] intact) so thumb color
matches the track logic.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
sources/modal/components/WebAlertModal.tsx (1)

112-220: Verify 3‑button ordering in the twoPlusOne layout.

For 3 buttons, the UI renders indices 0 and 2 on the top row and 1 on the bottom, which reorders the semantic order from config.buttons. If any callers rely on positional meaning, this could swap actions. Consider documenting the expected order or selecting the primary button explicitly before layout.

sources/components/AgentInput.tsx (1)

498-557: Add isCodex/isGemini to handleKeyPress dependencies.

The callback uses these flags to build the mode order, but they’re not in the dependency list—if the flavor changes, the handler can stay stale.

🐛 Proposed fix
-    }, [suggestions, moveUp, moveDown, selected, handleSuggestionSelect, props.showAbortButton, props.onAbort, isAborting, handleAbortPress, agentInputEnterToSend, props.value, props.onSend, props.permissionMode, props.onPermissionModeChange]);
+    }, [suggestions, moveUp, moveDown, selected, handleSuggestionSelect, props.showAbortButton, props.onAbort, isAborting, handleAbortPress, agentInputEnterToSend, props.value, props.onSend, props.permissionMode, props.onPermissionModeChange, isCodex, isGemini]);
🤖 Fix all issues with AI agents
In `@sources/app/`(app)/new/pick/profile.tsx:
- Around line 330-336: The built-in row currently calls
setProfileParamAndClose(profile.id) directly which bypasses the
ignoreNextRowPress guard; update the Item's onPress to call
handleProfileRowPress so the suppression logic runs (i.e., route the built-in
profile row press through handleProfileRowPress and pass the profile id and the
existing setter callback such that handleProfileRowPress invokes
setProfileParamAndClose(profile.id) when appropriate). Ensure you replace the
direct call in the Item onPress with a call to handleProfileRowPress for
consistency with other rows.

In `@sources/components/EnvironmentVariablesList.tsx`:
- Around line 42-57: The resolvedEnvVarRefs logic currently only filters by
SECRET_NAME_REGEX; update the useMemo that computes resolvedEnvVarRefs to also
skip variables documented as secrets (e.g., check profileDocs for an entry
matching the env var name or the extracted ref and if that entry has isSecret
=== true then return/skip). In other words, inside the forEach over
environmentVariables in resolvedEnvVarRefs, after extracting ref via
extractVarNameFromValue and before refs.add(ref), add a guard that looks up the
corresponding documented variable in profileDocs (by envVar.name or the
extracted ref) and skips adding when that documented entry has isSecret set.
Keep the existing SECRET_NAME_REGEX checks in place.

In `@sources/sync/sync.ts`:
- Around line 1506-1539: When updating session thinking state in the
applySessions call, also update the thinkingAt timestamp so it reflects the
lifecycle event time; inside the object passed to applySessions (near symbols
session, updateData, isTaskComplete, isTaskStarted, applySessions) add a
thinkingAt: updateData.createdAt when you set thinking to true or false (i.e.,
when isTaskStarted or isTaskComplete is true) so the timeline remains consistent
if activity updates are dropped.
♻️ Duplicate comments (2)
sources/app/(app)/new/pick/profile.tsx (1)

163-168: Extract remaining inline styles into the Unistyles stylesheet.

The checkmark opacity style and ItemList padding are still inline. Please move them into the existing stylesheet. As per coding guidelines, use Unistyles for all styling.

Also applies to: 237-237

sources/components/EnvironmentVariableCard.tsx (1)

100-105: Sync local state when variable.value changes externally.
useRemoteVariable, remoteVariableName, and defaultValue are only initialized once. If variable.value is updated by the parent (profile load/reset), the card can display stale values.

🧩 Possible fix
 const parsed = parseVariableValue(variable.value);
 const [useRemoteVariable, setUseRemoteVariable] = React.useState(parsed.useRemoteVariable);
 const [remoteVariableName, setRemoteVariableName] = React.useState(parsed.remoteVariableName);
 const [defaultValue, setDefaultValue] = React.useState(parsed.defaultValue);
 const fallbackOperator = parsed.fallbackOperator;

+React.useEffect(() => {
+    const next = parseVariableValue(variable.value);
+    if (next.useRemoteVariable !== useRemoteVariable) setUseRemoteVariable(next.useRemoteVariable);
+    if (next.remoteVariableName !== remoteVariableName) setRemoteVariableName(next.remoteVariableName);
+    if (next.defaultValue !== defaultValue) setDefaultValue(next.defaultValue);
+}, [variable.value]);
🧹 Nitpick comments (7)
sources/utils/promptUnsavedChangesAlert.test.ts (2)

1-3: Use the @/ path alias for local imports.*

Line 3 should use the configured alias to stay consistent with project conventions.

♻️ Suggested change
-import { promptUnsavedChangesAlert } from './promptUnsavedChangesAlert';
+import { promptUnsavedChangesAlert } from '@/utils/promptUnsavedChangesAlert';

6-24: Reduce duplication and assert button presence before invoking onPress.

Optional but helpful: extract a tiny harness to avoid repeating the same setup and to fail fast if the expected button is missing (instead of a timeout).

♻️ Suggested refactor
 describe('promptUnsavedChangesAlert', () => {
+    const setupAlert = () => {
+        let lastButtons: AlertButton[] | undefined;
+
+        const alert = (_title: string, _message?: string, buttons?: AlertButton[]) => {
+            lastButtons = buttons;
+        };
+
+        const promise = promptUnsavedChangesAlert(alert, {
+            title: 'Discard changes',
+            message: 'You have unsaved changes.',
+            discardText: 'Discard',
+            saveText: 'Save',
+            keepEditingText: 'Keep editing',
+        });
+
+        const press = (text: string) => {
+            const button = lastButtons?.find((b) => b.text === text);
+            expect(button).toBeTruthy();
+            button?.onPress?.();
+        };
+
+        return { promise, press };
+    };
+
     it('resolves to save when the Save button is pressed', async () => {
-        let lastButtons: AlertButton[] | undefined;
-
-        const alert = (_title: string, _message?: string, buttons?: AlertButton[]) => {
-            lastButtons = buttons;
-        };
-
-        const promise = promptUnsavedChangesAlert(alert, {
-            title: 'Discard changes',
-            message: 'You have unsaved changes.',
-            discardText: 'Discard',
-            saveText: 'Save',
-            keepEditingText: 'Keep editing',
-        });
-
-        lastButtons?.find((b) => b.text === 'Save')?.onPress?.();
+        const { promise, press } = setupAlert();
+        press('Save');
 
         await expect(promise).resolves.toBe('save');
     });
 
     it('resolves to discard when the Discard button is pressed', async () => {
-        let lastButtons: AlertButton[] | undefined;
-
-        const alert = (_title: string, _message?: string, buttons?: AlertButton[]) => {
-            lastButtons = buttons;
-        };
-
-        const promise = promptUnsavedChangesAlert(alert, {
-            title: 'Discard changes',
-            message: 'You have unsaved changes.',
-            discardText: 'Discard',
-            saveText: 'Save',
-            keepEditingText: 'Keep editing',
-        });
-
-        lastButtons?.find((b) => b.text === 'Discard')?.onPress?.();
+        const { promise, press } = setupAlert();
+        press('Discard');
 
         await expect(promise).resolves.toBe('discard');
     });
 
     it('resolves to keepEditing when the Keep editing button is pressed', async () => {
-        let lastButtons: AlertButton[] | undefined;
-
-        const alert = (_title: string, _message?: string, buttons?: AlertButton[]) => {
-            lastButtons = buttons;
-        };
-
-        const promise = promptUnsavedChangesAlert(alert, {
-            title: 'Discard changes',
-            message: 'You have unsaved changes.',
-            discardText: 'Discard',
-            saveText: 'Save',
-            keepEditingText: 'Keep editing',
-        });
-
-        lastButtons?.find((b) => b.text === 'Keep editing')?.onPress?.();
+        const { promise, press } = setupAlert();
+        press('Keep editing');
 
         await expect(promise).resolves.toBe('keepEditing');
     });
 });

Also applies to: 26-44, 46-64

sources/components/newSession/MachineSelector.tsx (1)

38-42: Default prop values with t() calls are evaluated at module load time.

These default values are evaluated once when the module is loaded, not on each render. If the app supports dynamic language switching, users won't see updated translations until the component remounts.

Consider moving these defaults inside the component body or using nullish coalescing:

♻️ Suggested approach
 export function MachineSelector({
     machines,
     selectedMachine,
     recentMachines = [],
     favoriteMachines = [],
     onSelect,
     onToggleFavorite,
     showFavorites = true,
     showRecent = true,
     showSearch = true,
     searchPlacement = 'header',
-    searchPlaceholder = t('newSession.machinePicker.searchPlaceholder'),
-    recentSectionTitle = t('newSession.machinePicker.recentTitle'),
-    favoritesSectionTitle = t('newSession.machinePicker.favoritesTitle'),
-    allSectionTitle = t('newSession.machinePicker.allTitle'),
-    noItemsMessage = t('newSession.machinePicker.emptyMessage'),
+    searchPlaceholder,
+    recentSectionTitle,
+    favoritesSectionTitle,
+    allSectionTitle,
+    noItemsMessage,
 }: MachineSelectorProps) {
     const { theme } = useUnistyles();
+
+    const resolvedSearchPlaceholder = searchPlaceholder ?? t('newSession.machinePicker.searchPlaceholder');
+    const resolvedRecentSectionTitle = recentSectionTitle ?? t('newSession.machinePicker.recentTitle');
+    const resolvedFavoritesSectionTitle = favoritesSectionTitle ?? t('newSession.machinePicker.favoritesTitle');
+    const resolvedAllSectionTitle = allSectionTitle ?? t('newSession.machinePicker.allTitle');
+    const resolvedNoItemsMessage = noItemsMessage ?? t('newSession.machinePicker.emptyMessage');
sources/sync/modelOptions.ts (1)

12-33: Consider memoizing or extracting Gemini options if called frequently in render paths.

The function creates a new array with new objects on each call. If getModelOptionsForAgentType('gemini') is called during renders, this could cause unnecessary re-renders in consuming components that depend on referential equality.

If this becomes a performance concern, consider memoizing or caching the result:

♻️ Optional optimization
// Cache outside the function to maintain referential stability
let cachedGeminiOptions: readonly ModelOption[] | null = null;

export function getModelOptionsForAgentType(agentType: AgentType): readonly ModelOption[] {
    if (agentType === 'gemini') {
        if (!cachedGeminiOptions) {
            cachedGeminiOptions = [
                // ... options
            ];
        }
        return cachedGeminiOptions;
    }
    return [];
}
sources/components/EnvironmentVariablesList.tsx (1)

64-75: Move inline styles into a Unistyles stylesheet.

The new layout blocks (header, list container, add form, inputs, and button) and webNoOutline are still inline. Please extract them into StyleSheet.create(() => ({})) (function mode where theme/runtime is needed) and keep the stylesheet at the end of the file. As per coding guidelines, use Unistyles for all styling.

Also applies to: 151-210, 232-288

sources/components/EnvironmentVariableCard.tsx (1)

73-98: Prefer StyleSheet.create (function mode) for new styles.
The new useMemo style objects/inline blocks should move into Unistyles to match project rules.

♻️ Example refactor direction
-import { useUnistyles } from 'react-native-unistyles';
+import { useUnistyles, StyleSheet } from 'react-native-unistyles';

-const webNoOutline = React.useMemo(() => (Platform.select({
-    web: { /* ... */ },
-    default: {},
-}) as object), []);
-
-const secondaryTextStyle = React.useMemo(() => ({
-    fontSize: Platform.select({ ios: 15, default: 14 }),
-    lineHeight: 20,
-    letterSpacing: Platform.select({ ios: -0.24, default: 0.1 }),
-    ...Typography.default(),
-}), []);
+const styles = StyleSheet.create(() => ({
+    webNoOutline: Platform.select({
+        web: { /* ... */ },
+        default: {},
+    }) as object,
+    secondaryText: {
+        fontSize: Platform.select({ ios: 15, default: 14 }),
+        lineHeight: 20,
+        letterSpacing: Platform.select({ ios: -0.24, default: 0.1 }),
+        ...Typography.default(),
+    },
+}));

As per coding guidelines, use StyleSheet.create for styling and function mode when runtime/platform access is needed.

sources/text/translations/ja.ts (1)

16-18: Unused plural helper function.

This function is defined but never called in the file. While Japanese doesn't require grammatical plural forms, consider removing this function to reduce dead code, or add a comment explaining it's kept for consistency with other translation files.

Comment on lines 1506 to 1539
// Check for task lifecycle events to update thinking state
// This ensures UI updates even if volatile activity updates are lost
const rawContent = decrypted.content as { role?: string; content?: { type?: string; data?: { type?: string } } } | null;
const contentType = rawContent?.content?.type;
const dataType = rawContent?.content?.data?.type;

// Debug logging to trace lifecycle events
const isDev = typeof __DEV__ !== 'undefined' && __DEV__;
if (isDev && (dataType === 'task_complete' || dataType === 'turn_aborted' || dataType === 'task_started')) {
console.log(`🔄 [Sync] Lifecycle event detected: contentType=${contentType}, dataType=${dataType}`);
}

const isTaskComplete =
((contentType === 'acp' || contentType === 'codex') &&
(dataType === 'task_complete' || dataType === 'turn_aborted'));

const isTaskStarted =
((contentType === 'acp' || contentType === 'codex') && dataType === 'task_started');

if (isDev && (isTaskComplete || isTaskStarted)) {
console.log(`🔄 [Sync] Updating thinking state: isTaskComplete=${isTaskComplete}, isTaskStarted=${isTaskStarted}`);
}

// Update session
const session = storage.getState().sessions[updateData.body.sid];
if (session) {
this.applySessions([{
...session,
updatedAt: updateData.createdAt,
seq: updateData.seq
seq: updateData.seq,
// Update thinking state based on task lifecycle events
...(isTaskComplete ? { thinking: false } : {}),
...(isTaskStarted ? { thinking: true } : {})
}])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update thinkingAt when lifecycle events toggle thinking.

If activity updates are dropped, thinkingAt can remain stale even though thinking is flipped here. Setting it to updateData.createdAt keeps the timeline consistent.

🐛 Proposed fix
-                            ...(isTaskComplete ? { thinking: false } : {}),
-                            ...(isTaskStarted ? { thinking: true } : {})
+                            ...(isTaskComplete ? { thinking: false, thinkingAt: updateData.createdAt } : {}),
+                            ...(isTaskStarted ? { thinking: true, thinkingAt: updateData.createdAt } : {})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check for task lifecycle events to update thinking state
// This ensures UI updates even if volatile activity updates are lost
const rawContent = decrypted.content as { role?: string; content?: { type?: string; data?: { type?: string } } } | null;
const contentType = rawContent?.content?.type;
const dataType = rawContent?.content?.data?.type;
// Debug logging to trace lifecycle events
const isDev = typeof __DEV__ !== 'undefined' && __DEV__;
if (isDev && (dataType === 'task_complete' || dataType === 'turn_aborted' || dataType === 'task_started')) {
console.log(`🔄 [Sync] Lifecycle event detected: contentType=${contentType}, dataType=${dataType}`);
}
const isTaskComplete =
((contentType === 'acp' || contentType === 'codex') &&
(dataType === 'task_complete' || dataType === 'turn_aborted'));
const isTaskStarted =
((contentType === 'acp' || contentType === 'codex') && dataType === 'task_started');
if (isDev && (isTaskComplete || isTaskStarted)) {
console.log(`🔄 [Sync] Updating thinking state: isTaskComplete=${isTaskComplete}, isTaskStarted=${isTaskStarted}`);
}
// Update session
const session = storage.getState().sessions[updateData.body.sid];
if (session) {
this.applySessions([{
...session,
updatedAt: updateData.createdAt,
seq: updateData.seq
seq: updateData.seq,
// Update thinking state based on task lifecycle events
...(isTaskComplete ? { thinking: false } : {}),
...(isTaskStarted ? { thinking: true } : {})
}])
// Check for task lifecycle events to update thinking state
// This ensures UI updates even if volatile activity updates are lost
const rawContent = decrypted.content as { role?: string; content?: { type?: string; data?: { type?: string } } } | null;
const contentType = rawContent?.content?.type;
const dataType = rawContent?.content?.data?.type;
// Debug logging to trace lifecycle events
const isDev = typeof __DEV__ !== 'undefined' && __DEV__;
if (isDev && (dataType === 'task_complete' || dataType === 'turn_aborted' || dataType === 'task_started')) {
console.log(`🔄 [Sync] Lifecycle event detected: contentType=${contentType}, dataType=${dataType}`);
}
const isTaskComplete =
((contentType === 'acp' || contentType === 'codex') &&
(dataType === 'task_complete' || dataType === 'turn_aborted'));
const isTaskStarted =
((contentType === 'acp' || contentType === 'codex') && dataType === 'task_started');
if (isDev && (isTaskComplete || isTaskStarted)) {
console.log(`🔄 [Sync] Updating thinking state: isTaskComplete=${isTaskComplete}, isTaskStarted=${isTaskStarted}`);
}
// Update session
const session = storage.getState().sessions[updateData.body.sid];
if (session) {
this.applySessions([{
...session,
updatedAt: updateData.createdAt,
seq: updateData.seq,
// Update thinking state based on task lifecycle events
...(isTaskComplete ? { thinking: false, thinkingAt: updateData.createdAt } : {}),
...(isTaskStarted ? { thinking: true, thinkingAt: updateData.createdAt } : {})
}])
🤖 Prompt for AI Agents
In `@sources/sync/sync.ts` around lines 1506 - 1539, When updating session
thinking state in the applySessions call, also update the thinkingAt timestamp
so it reflects the lifecycle event time; inside the object passed to
applySessions (near symbols session, updateData, isTaskComplete, isTaskStarted,
applySessions) add a thinkingAt: updateData.createdAt when you set thinking to
true or false (i.e., when isTaskStarted or isTaskComplete is true) so the
timeline remains consistent if activity updates are dropped.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@sources/components/EnvironmentVariableCard.tsx`:
- Around line 93-118: The effect and preview logic use fallbackOperator directly
which can be null and drop the fallback even when defaultValue is non-empty;
derive a single computed operator once (e.g., compute a local operator from
fallbackOperator and defaultValue) and reuse it in React.useEffect,
computedTemplateValue, and resolvedSessionValue where formatEnvVarTemplate is
called (references: useRemoteVariable, remoteVariableName, defaultValue,
fallbackOperator, formatEnvVarTemplate, computedTemplateValue,
resolvedSessionValue, React.useEffect) so that when a user enters a fallback the
template always includes the correct operator/fallback behavior instead of
silently producing ${VAR} without the fallback.

In `@sources/components/EnvironmentVariablesList.tsx`:
- Around line 158-165: The current logic uses getDocumentation(varNameFromValue
|| envVar.name) which ignores documentation attached to envVar.name when the
value references another var; update to fetch both docs: call
getDocumentation(envVar.name) (primaryDocs) and, if varNameFromValue is present,
getDocumentation(varNameFromValue) (refDocs), then compute isSecret as true if
primaryDocs.isSecret || refDocs?.isSecret || SECRET_NAME_REGEX.test(envVar.name)
|| SECRET_NAME_REGEX.test(varNameFromValue || ''); for display/metadata prefer
primaryDocs but include refDocs where needed (e.g., show reference info or
combine flags) so documented secrets on the variable name are preserved even
when the value is a reference.
🧹 Nitpick comments (6)
sources/types/react-test-renderer.d.ts (1)

1-1: Bare module declaration bypasses type checking; note that react-test-renderer is deprecated in React 19.

This ambient declaration provides no type information, making all imports from react-test-renderer resolve to any. Per the coding guidelines, code should be properly typed.

Additionally, based on the provided React 19 documentation, react-test-renderer is deprecated—the recommended migration path is to use @testing-library/react or @testing-library/react-native.

Consider one of the following:

  1. Install @types/react-test-renderer for proper typings if you need to keep using this package temporarily.
  2. Migrate test code to React Testing Library and remove this declaration.
sources/app/(app)/new/pick/profile.tsx (3)

3-3: Remove unused import.

Pressable is imported but never used in this component.

-import { Pressable, View } from 'react-native';
+import { View } from 'react-native';

23-23: Remove unused variable.

selectedIndicatorColor is defined but never used. The checkmark icons reference theme.colors.text directly.

-    const selectedIndicatorColor = rt.themeName === 'dark' ? theme.colors.text : theme.colors.button.primary.background;

147-190: Inconsistent indentation in callback.

The indentation within renderProfileRowRightElement is inconsistent—some lines appear to use tabs while others use spaces. Per coding guidelines, use 4 spaces for indentation consistently.

sources/components/AgentInput.tsx (1)

689-755: Extract new inline styles into StyleSheet.create.

New UI blocks (model options + action chip rows) introduce multiple inline style objects. Per repo guidance, extract the static styles into StyleSheet.create and keep only pressed-state overrides inline. As per coding guidelines, all styling should come from Unistyles.

♻️ Example refactor (model options)
 const stylesheet = StyleSheet.create((theme, runtime) => ({
+    modelOptionItem: {
+        flexDirection: 'row',
+        alignItems: 'center',
+        paddingHorizontal: 16,
+        paddingVertical: 8,
+    },
+    modelOptionItemPressed: {
+        backgroundColor: theme.colors.surfacePressed,
+    },
+    modelOptionLabel: {
+        fontSize: 14,
+        ...Typography.default(),
+    },
+    modelOptionDescription: {
+        fontSize: 11,
+        color: theme.colors.textSecondary,
+        ...Typography.default(),
+    },
 }));
-    <Pressable
-        style={({ pressed }) => ({
-            flexDirection: 'row',
-            alignItems: 'center',
-            paddingHorizontal: 16,
-            paddingVertical: 8,
-            backgroundColor: pressed ? theme.colors.surfacePressed : 'transparent'
-        })}
-    >
+    <Pressable
+        style={({ pressed }) => [
+            styles.modelOptionItem,
+            pressed && styles.modelOptionItemPressed
+        ]}
+    >
...
-        <Text style={{
-            fontSize: 14,
-            color: isSelected ? theme.colors.radio.active : theme.colors.text,
-            ...Typography.default()
-        }}>
+        <Text style={[
+            styles.modelOptionLabel,
+            { color: isSelected ? theme.colors.radio.active : theme.colors.text }
+        ]}>
...
-        <Text style={{
-            fontSize: 11,
-            color: theme.colors.textSecondary,
-            ...Typography.default()
-        }}>
+        <Text style={styles.modelOptionDescription}>

Also applies to: 840-1195

sources/components/EnvironmentVariableCard.test.ts (1)

1-2: React-test-renderer is deprecated in React 19; consider @testing-library/react-native for new tests.

The repo is on React 19.1.0 where react-test-renderer is officially deprecated. This affects multiple test files in the codebase (EnvironmentVariablesList.test.ts also uses it). When refactoring test coverage, migrate to @testing-library/react-native for consistency and forward compatibility.

Comment on lines 93 to 118
// Update parent when local state changes
React.useEffect(() => {
const newValue = useRemoteVariable && remoteVariableName.trim() !== ''
? `\${${remoteVariableName}${defaultValue ? `:-${defaultValue}` : ''}}`
? formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue, operator: fallbackOperator })
: defaultValue;

if (newValue !== variable.value) {
onUpdate(newValue);
onUpdate(index, newValue);
}
}, [useRemoteVariable, remoteVariableName, defaultValue, variable.value, onUpdate]);
}, [defaultValue, fallbackOperator, index, onUpdate, remoteVariableName, useRemoteVariable, variable.value]);

// Determine status
const showRemoteDiffersWarning = remoteValue !== null && expectedValue && remoteValue !== expectedValue;
const showDefaultOverrideWarning = expectedValue && defaultValue !== expectedValue;

const computedTemplateValue =
useRemoteVariable && remoteVariableName.trim() !== ''
? formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue, operator: fallbackOperator })
: defaultValue;

const resolvedSessionValue =
isSecret
? (useRemoteVariable && remoteVariableName
? t('profiles.environmentVariables.preview.secretValueHidden', {
value: formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue !== '' ? '***' : '', operator: fallbackOperator }),
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure fallback templates include defaults when users enter a fallback.

When fallbackOperator is null (e.g., toggling from a literal value) but defaultValue is non‑empty, you pass operator: null, which forces ${VAR} and drops the fallback while the UI preview still treats defaultValue as a fallback. This can silently misconfigure the session value.

✅ Suggested fix (derive operator once and reuse)
-    React.useEffect(() => {
+    const templateOperator =
+        fallbackOperator ?? (defaultValue.trim() !== '' ? ':-' : undefined);
+
+    React.useEffect(() => {
         const newValue = useRemoteVariable && remoteVariableName.trim() !== ''
-            ? formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue, operator: fallbackOperator })
+            ? formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue, operator: templateOperator })
             : defaultValue;
 
         if (newValue !== variable.value) {
             onUpdate(index, newValue);
         }
-    }, [defaultValue, fallbackOperator, index, onUpdate, remoteVariableName, useRemoteVariable, variable.value]);
+    }, [defaultValue, fallbackOperator, index, onUpdate, remoteVariableName, useRemoteVariable, variable.value]);
 
     const computedTemplateValue =
         useRemoteVariable && remoteVariableName.trim() !== ''
-            ? formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue, operator: fallbackOperator })
+            ? formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue, operator: templateOperator })
             : defaultValue;
 
     const resolvedSessionValue =
         isSecret
             ? (useRemoteVariable && remoteVariableName
                 ? t('profiles.environmentVariables.preview.secretValueHidden', {
-                    value: formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue !== '' ? '***' : '', operator: fallbackOperator }),
+                    value: formatEnvVarTemplate({ sourceVar: remoteVariableName, fallback: defaultValue !== '' ? '***' : '', operator: templateOperator }),
                 })
             : (defaultValue ? t('profiles.environmentVariables.preview.hiddenValue') : emptyValue))
             : (useRemoteVariable && machineId && remoteValue !== undefined
                 ? (remoteValue === null || remoteValue === '' ? (hasFallback ? defaultValue : emptyValue) : remoteValue)
                 : (computedTemplateValue || emptyValue));
🤖 Prompt for AI Agents
In `@sources/components/EnvironmentVariableCard.tsx` around lines 93 - 118, The
effect and preview logic use fallbackOperator directly which can be null and
drop the fallback even when defaultValue is non-empty; derive a single computed
operator once (e.g., compute a local operator from fallbackOperator and
defaultValue) and reuse it in React.useEffect, computedTemplateValue, and
resolvedSessionValue where formatEnvVarTemplate is called (references:
useRemoteVariable, remoteVariableName, defaultValue, fallbackOperator,
formatEnvVarTemplate, computedTemplateValue, resolvedSessionValue,
React.useEffect) so that when a user enters a fallback the template always
includes the correct operator/fallback behavior instead of silently producing
${VAR} without the fallback.

Comment on lines 158 to 165
{environmentVariables.map((envVar, index) => {
const varNameFromValue = extractVarNameFromValue(envVar.value);
const docs = getDocumentation(varNameFromValue || envVar.name);
const isSecret =
docs.isSecret ||
SECRET_NAME_REGEX.test(envVar.name) ||
SECRET_NAME_REGEX.test(varNameFromValue || '');

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Documented secrets can be lost when the value references another var.

docs is derived from varNameFromValue || envVar.name, so a documented secret on envVar.name is ignored when the value is ${OTHER}. That drops secret handling in the card (lock icon, masked input, preview).

✅ Suggested fix (use envVar.name docs + optional ref docs)
-                        const docs = getDocumentation(varNameFromValue || envVar.name);
-                        const isSecret =
-                            docs.isSecret ||
-                            SECRET_NAME_REGEX.test(envVar.name) ||
-                            SECRET_NAME_REGEX.test(varNameFromValue || '');
+                        const docs = getDocumentation(envVar.name);
+                        const refDocs = varNameFromValue ? getDocumentation(varNameFromValue) : undefined;
+                        const isSecret =
+                            docs.isSecret ||
+                            refDocs?.isSecret ||
+                            SECRET_NAME_REGEX.test(envVar.name) ||
+                            SECRET_NAME_REGEX.test(varNameFromValue || '');

Also applies to: 167-177

🤖 Prompt for AI Agents
In `@sources/components/EnvironmentVariablesList.tsx` around lines 158 - 165, The
current logic uses getDocumentation(varNameFromValue || envVar.name) which
ignores documentation attached to envVar.name when the value references another
var; update to fetch both docs: call getDocumentation(envVar.name) (primaryDocs)
and, if varNameFromValue is present, getDocumentation(varNameFromValue)
(refDocs), then compute isSecret as true if primaryDocs.isSecret ||
refDocs?.isSecret || SECRET_NAME_REGEX.test(envVar.name) ||
SECRET_NAME_REGEX.test(varNameFromValue || ''); for display/metadata prefer
primaryDocs but include refDocs where needed (e.g., show reference info or
combine flags) so documented secrets on the variable name are preserved even
when the value is a reference.

@leeroybrun leeroybrun closed this Jan 17, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 17, 2026

Tip

For best results, initiate chat on the files or code changes.

@leeroybrun I'll review the final state of the PR focusing on the non-Gemini changes that address PR #272 regressions. Let me provide a comprehensive review organized by major areas:

✅ New Session UX Restoration

Excellent work restoring the original New Session experience:

  • Reintroduced independent feature flags (useProfiles vs useEnhancedSessionWizard) and made machine/path search opt-in
  • Modal presentation properly restored with presentation: 'modal' and appropriate header controls
  • Removed the large NewSessionWizard.tsx component entirely, simplifying the codebase

✅ Feature Gating & Settings

Well-implemented opt-in strategy:

  • New settings flags: useProfiles, useMachinePickerSearch, usePathPickerSearch
  • Settings parsing hardened to avoid whole-object fallback on single-field errors
  • Proper UI gating throughout (profiles menu items, picker search controls)

✅ Profile Management Improvements

Significant architectural improvements:

  • Profile editing/navigation switched to id-based routing (unmount-safe, UUID-backed for new profiles)
  • Prevents persisting invalid profiles with id: ''
  • Proper favorites management with favoriteProfiles setting
  • Built-in vs custom profile distinction properly maintained
  • Profile picker modal is lightweight and properly isolated

Good patterns:

  • buildProfileGroups and buildProfileActions provide clean separation
  • Duplicate name validation with user-facing alerts
  • Unsaved changes flow with promptUnsavedChangesAlert

✅ Environment Variable Safety

Critical security improvements:

  • Removes global daemon env fetch that queried all profile ${VAR} refs; scopes env resolution and previews
  • Strengthens secret heuristics (adds PASS|PASSWORD|COOKIE)
  • Ensures env previews do not fetch secret values into UI memory
  • Standardizes template syntax to ${VAR} and ${VAR:-default}
  • Distinguishes unset vs empty via JSON/node fallback and avoids stdout trimming

Well-designed components:

  • EnvironmentVariableCard now receives machineEnv as props (no internal fetching)
  • Preview modal properly masks secrets
  • Environment variable references are filtered to exclude secret-like names

✅ Component Refactoring

Good modular design:

  • New reusable components: SearchHeader, MachineSelector, PathSelector, ItemRowActions
  • ItemGroup with dividers helper provides clean list rendering
  • Profile compatibility icon component for backend support display
  • Proper separation of concerns throughout

✅ Type Safety & Schema

Strong type improvements:

  • Removed legacy provider-specific config schemas from AIBackendProfileSchema
  • New permissionTypes.ts and modelOptions.ts modules centralize types
  • Profile mutations properly typed with dedicated helpers
  • Modal system now has proper generic typing for injected props

⚠️ Minor Observations

  1. Settings migration: Legacy provider configs are migrated to environmentVariables - ensure this migration is well-tested across various user configurations

  2. Session model modes: New persistence for session-specific model modes - the storage key management looks good with proper cleanup on session deletion

  3. Web-specific handling: Several web-specific adjustments (event propagation, styling) - these look correct but ensure thorough web testing

📝 Documentation & Testing

Good test coverage additions:

  • Profile grouping, routing params, environment variables
  • Message metadata building
  • Permission mode mapping
  • Various utility functions

Overall Assessment

This is a well-executed stabilization PR that:

  1. ✅ Properly gates new features as opt-in
  2. ✅ Fixes serious security issues with env var handling
  3. ✅ Restores original UX while keeping improvements available
  4. ✅ Improves type safety and architectural patterns
  5. ✅ Adds proper test coverage for new utilities

The changes show careful attention to backward compatibility, security, and maintainability. The id-based routing for profiles and scoped env var resolution are particularly good improvements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant