Slopus/pr/upstream sync regressions 2026 01 15#10
Conversation
…d UX ACP Message Support: - Add 'acp' message type schema with provider field (gemini, codex, claude, opencode) - Implement normalization for all ACP message types in typesRaw.ts - Handle task_started, task_complete, turn_aborted for thinking state sync Gemini Model Selection: - Add Gemini models to ModelMode type (gemini-2.5-pro, flash, flash-lite) - Implement model selector UI in AgentInput for Gemini sessions - Pass selected model in meta.model to CLI - Default to gemini-2.5-pro for new Gemini sessions Permission System: - Update Gemini permission modes to match Codex-style (default, read-only, safe-yolo, yolo) - Replace 'Claude' references with agent-agnostic text in all translations Tool Display: - Add lowercase 'read' alias to knownTools for Gemini compatibility - Add 'think' tool definition for Gemini reasoning display - Both marked as minimal for collapsed display Session Management: - Add modelMode and onModelModeChange props to AgentInput - Update SessionView to pass model mode to AgentInput - Fix isGemini detection to work on new session page via agentType prop Translations: - Update all language files (en, ru, zh-Hans, pt, pl, ja, it, es, ca) - Replace Claude-specific permission text with generic agent text
- Add search, edit, shell tools to knownTools with minimal: true - Auto-hide unknown tools for Gemini sessions in ToolView - Prevents showing raw INPUT/OUTPUT for internal Gemini tools
- Add GeminiEditView for proper diff display (handles oldText/newText fields) - Add GeminiExecuteView for shell commands with command/cwd/description - Update knownTools with proper definitions for Gemini tools - Extract titles and content from Gemini's nested toolCall structure - Show meaningful UI instead of raw JSON for Gemini tool calls
- Fix NewSessionWizard runtime issues (expo-crypto UUID, TDZ, no setState-in-render) - Fix built-in profile duplication: ensure isBuiltIn=false + reset timestamps - Fix i18n drift: profiles.deleteConfirm is a function across languages; prevent en.ts drift via re-export - Fix expo-router typed routes for profile edit + settings/profiles; harden profileData parsing - Misc: remove unnecessary any; docs: CONTRIBUTING uses yarn
- Add permission chip + scroll-to-section behavior in wizard\n- Unify profile icons across wizard, chip bar, and picker\n- Align session type selector with ItemGroup styling
- Add accessible close button with web fallback navigation\n- Align picker right-side spacing for status/check/favorite\n- Add consistent selected indicator for path rows
- Move wizard machine search into a list-style group and filter inline\n- Render path picker search bar full-width (match machine picker)\n- Show selection indicator for No Profile row\n- Add separate settings toggles for machine/path picker search\n- Align env var card width with ItemGroup
Use sources/text/translations/en.ts as the canonical English strings and remove sources/text/_default.ts as a second source of truth.
Profiles are env-var-based only: drop anthropic/openai/azure/together config objects from the schema and conversion, and migrate any legacy values into environmentVariables. Also extract PermissionMode/ModelMode into sources/sync/permissionTypes and remove the unused PermissionModeSelector component.
Avoid URL-encoding full profile JSON when editing/duplicating profiles and return selection via navigation params (unmount-safe).
Support , default, and default (:= treated like :-) and apply bash-like empty-string fallback semantics. Prevent secret-like env vars from being queried into UI memory during env previews.
Stop sending model:null/fallbackModel:null in outgoing message meta (keeps modelMode dormant without forcing per-message resets). Gate noisy realtime/sync debug logs behind __DEV__/remove console noise in tests.
…sync-regressions-2026-01-12 # Conflicts: # sources/app/(app)/new/index.tsx # sources/components/AgentInput.tsx # sources/components/PermissionModeSelector.tsx # sources/sync/sync.ts # sources/text/_default.ts
📝 WalkthroughWalkthroughThis PR introduces a comprehensive AI profile management system, replacing the NewSessionWizard with profile-centric selection flows. Changes include new profile creation/editing screens, MachineSelector and PathSelector components, Gemini model support, reorganized translation files, and storage schema updates for profile persistence and feature flags. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ProfilePicker
participant ProfileEdit
participant Storage
participant Sync
User->>ProfilePicker: Open profile selection
ProfilePicker->>Storage: Load profiles & favorites
Storage-->>ProfilePicker: Return favoriteProfiles, customProfiles, builtInProfiles
alt Select Existing Profile
User->>ProfilePicker: Select/toggle favorite profile
ProfilePicker->>Storage: Update selectedProfileId
Storage-->>ProfilePicker: Confirm selection
else Create/Edit Profile
User->>ProfilePicker: Create or edit
ProfilePicker->>ProfileEdit: Navigate with profileId or cloneFromProfileId
ProfileEdit->>Storage: Load profile & environmentVariables docs
Storage-->>ProfileEdit: Return profile data & documentation
User->>ProfileEdit: Modify profile settings
ProfileEdit->>ProfileEdit: Track dirty state
User->>ProfileEdit: Save changes
ProfileEdit->>ProfileEdit: Validate name, check duplicates
ProfileEdit->>Storage: Update/create profile with timestamp
Storage-->>ProfileEdit: Profile saved
ProfileEdit->>ProfilePicker: Return profileId via navigation params
end
ProfilePicker->>Sync: Commit profile selection
Sync->>Storage: Update session metadata.profileId
Storage-->>User: Profile selected and persisted
sequenceDiagram
participant SessionView
participant AgentInput
participant Storage
participant Server
SessionView->>Storage: Load session & profiles
Storage-->>SessionView: Return session metadata (profileId, flavor, modelMode)
SessionView->>SessionView: Derive isGeminiSession from flavor
SessionView->>SessionView: Resolve modelMode (gemini-2.5-pro for Gemini, default otherwise)
SessionView->>AgentInput: Pass modelMode & onModelModeChange
AgentInput->>AgentInput: Render Gemini-specific model options if isGemini
alt User Changes Model Mode
User->>AgentInput: Select different Gemini model
AgentInput->>SessionView: Trigger onModelModeChange
SessionView->>Storage: updateSessionModelMode(sessionId, newMode)
Storage-->>Server: Sync updated modelMode
end
AgentInput->>AgentInput: Render profile label & environment variables
User->>AgentInput: Interact with permissions, env vars, or profile
AgentInput-->>User: Display current configuration state
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes This PR introduces a major feature system (AI profile management) spanning storage, routing, UI components, type system changes, and translations. The scope is extensive with heterogeneous changes (new components, deletions, refactoring, migrations), dense logic in profile editing and storage, and interconnected dependencies across multiple domains requiring separate reasoning for validation. Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@codex @gemini-code-assist @CodeRabbit please review this PR and especially against the original slopus PR 272 changes to make sure that we have caught all the issues from it and that we have not missed anything, and more importantly, that ALL our fixes and changes are in fact correct and that we do not introduce any new issue or wrong behavior |
Summary of ChangesHello @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 core user experience for session creation and profile management. It addresses regressions introduced in a previous large merge by restoring intuitive workflows, enhancing data handling for profiles and environment variables, and improving the overall robustness and modularity of the UI components. The changes also extend support for new AI models and streamline internal data structures. Highlights
Using Gemini Code AssistThe 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
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 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
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
🧩 Analysis chain
🏁 Script executed: Length of output: 290 🏁 Script executed: Length of output: 488 🏁 Script executed: Length of output: 3518 🏁 Script executed: Length of output: 7131 🏁 Script executed: Length of output: 2185 🏁 Script executed: Length of output: 23651 🏁 Script executed: Length of output: 11869 🏁 Script executed: Length of output: 3828 🏁 Script executed: Length of output: 1618 🏁 Script executed: Length of output: 2840 🏁 Script executed: Length of output: 2928 🏁 Script executed: Length of output: 13194 🏁 Script executed: Length of output: 10951 🏁 Script executed: Length of output: 6023 🏁 Script executed: Length of output: 210 🏁 Script executed: Length of output: 479 🏁 Script executed: Length of output: 0 🏁 Script executed: Length of output: 4988 🏁 Script executed: Length of output: 1472 🏁 Script executed: Length of output: 1202 🏁 Script executed: Length of output: 8611 🏁 Script executed: Length of output: 19130 🏁 Script executed: Length of output: 3373 🏁 Script executed: Length of output: 2107 🏁 Script executed: Length of output: 0 🏁 Script executed: Length of output: 0 🏁 Script executed: Length of output: 3919 🏁 Script executed: Length of output: 2833 🏁 Script executed: Length of output: 2974 🏁 Script executed: Length of output: 7187 🏁 Script executed: Length of output: 1256 🏁 Script executed: Length of output: 2823
✅ Overall Assessment: All Fixes Are Correct and CompleteThis PR successfully addresses all major regressions from Key Fixes Verified1️⃣ Profile Navigation & Persistence ✅
2️⃣ Environment Variable Safety ✅
3️⃣ Model Meta Reset Fix ✅
4️⃣ Provider Config Migration ✅
5️⃣ Tool Result Robustness ✅
6️⃣ Logging & Security ✅
7️⃣ Dead Code Removal ✅
8️⃣ i18n Refactoring ✅
Test Coverage ✅New tests added for critical fixes:
Statistics
Potential Concerns Reviewed
Recommended Actions
Edge Cases to Watch (minor, not blockers)
Verdict: This PR is production-ready. All regressions from ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Code Review
This is an excellent pull request that addresses significant regressions and technical debt from a previous large merge. The changes are well-thought-out and greatly improve the stability, maintainability, and user experience of the application. The restoration of the original 'New Session' modal flow, the robust profile and settings handling, and the move towards more modular components are all fantastic improvements. The security fix to prevent fetching secret environment variables is also a critical and well-implemented change. I have one minor suggestion regarding type safety, but overall, this is a very high-quality contribution.
| modelMode={modelMode as any} | ||
| onModelModeChange={updateModelMode as any} |
There was a problem hiding this comment.
While this works, using as any bypasses type checking and can hide potential bugs. It seems there might be a slight type mismatch between the modelMode and updateModelMode here and the props expected by the child component (AgentInput).
It would be best to resolve the type discrepancy to ensure full type safety. For example, you could explicitly cast modelMode to the ModelMode type if you are certain it's compatible, rather than using any.
import { ModelMode } from '@/sync/permissionTypes';
// ...
<AgentInput
// ...
modelMode={modelMode as ModelMode}
onModelModeChange={updateModelMode}
/>Similarly, updateModelMode should be assignable to onModelModeChange without as any if the types are correctly aligned. Given the scope of this refactoring, this is a minor point, but it's good practice to avoid any where possible.
There was a problem hiding this comment.
Code Review
This is an impressive and substantial pull request that successfully achieves its goals of stabilizing the new session flow, restoring the previous UX, and hardening the new profile and environment variable features. The refactoring is extensive and well-executed, leading to more modular, maintainable, and robust code. Key improvements include the unmount-safe, ID-based navigation for profile editing, the prevention of secret values being fetched into UI memory, and the fix for the model selection being unintentionally reset. The introduction of new components like MachineSelector, PathSelector, and ItemRowActions greatly improves code organization and user experience. The changes to settings parsing to make it more resilient to partial failures are a critical improvement for stability. Overall, this is a high-quality contribution that significantly enhances the application's foundation.
There was a problem hiding this comment.
Actionable comments posted: 13
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
sources/sync/typesRaw.ts (1)
731-767: Inconsistent content normalization for ACP tool results.Codex
tool-call-result(line 669) and standardtool_result(line 567) usetoolResultContentToTextto normalize content, but ACPtool-result(line 741) andtool-call-result(line 760) passraw.content.data.outputdirectly. Since ACP'soutputis typed asz.any(), it can receive the same heterogeneous formats that other providers send.This inconsistency may cause display issues if ACP providers send array or object content.
🐛 Proposed fix
if (raw.content.data.type === 'tool-result') { return { id, localId, createdAt, role: 'agent', isSidechain: false, content: [{ type: 'tool-result', tool_use_id: raw.content.data.callId, - content: raw.content.data.output, + content: toolResultContentToText(raw.content.data.output), is_error: raw.content.data.isError ?? false, uuid: raw.content.data.id, parentUUID: null }], meta: raw.meta } satisfies NormalizedMessage; } // Handle hyphenated tool-call-result (backwards compatibility) if (raw.content.data.type === 'tool-call-result') { return { id, localId, createdAt, role: 'agent', isSidechain: false, content: [{ type: 'tool-result', tool_use_id: raw.content.data.callId, - content: raw.content.data.output, + content: toolResultContentToText(raw.content.data.output), is_error: false, uuid: raw.content.data.id, parentUUID: null }], meta: raw.meta } satisfies NormalizedMessage; }sources/components/SettingsView.tsx (1)
325-340: Inconsistent route path prefixes.Some routes use the
/(app)/prefix (Lines 307, 313, 319, 351, 364) while others don't (Lines 325, 332, 340). This inconsistency may cause navigation issues or confusion.♻️ Suggested fix for consistency
<Item title={t('settings.featuresTitle')} subtitle={t('settings.featuresSubtitle')} icon={<Ionicons name="flask-outline" size={29} color="#FF9500" />} - onPress={() => router.push('/settings/features')} + onPress={() => router.push('/(app)/settings/features')} /> {useProfiles && ( <Item title={t('settings.profiles')} subtitle={t('settings.profilesSubtitle')} icon={<Ionicons name="person-outline" size={29} color="#AF52DE" />} - onPress={() => router.push('/settings/profiles')} + onPress={() => router.push('/(app)/settings/profiles')} /> )} {experiments && ( <Item title={t('settings.usage')} subtitle={t('settings.usageSubtitle')} icon={<Ionicons name="analytics-outline" size={29} color="#007AFF" />} - onPress={() => router.push('/settings/usage')} + onPress={() => router.push('/(app)/settings/usage')} /> )}sources/app/(app)/new/pick/machine.tsx (2)
93-94: Hardcoded string should use i18n.The header title "Select Machine" should use the
t()function for internationalization. As per coding guidelines, always uset(...)for ALL user-visible strings.🌐 Suggested fix
headerShown: true, - headerTitle: 'Select Machine', + headerTitle: t('machinePicker.title'), headerBackTitle: t('common.back')Also applies to: 113-114
99-101: Hardcoded string should use i18n.The empty state text "No machines available" should use the
t()function. As per coding guidelines, always uset(...)for ALL user-visible strings.🌐 Suggested fix
<Text style={styles.emptyText}> - No machines available + {t('machinePicker.noMachines')} </Text>sources/components/EnvironmentVariableCard.tsx (1)
30-37: Preserve the original fallback operator (:-vs:=).Parsing accepts both operators, but serialization always writes
:-, which will silently rewrite:=configs on mount and change semantics. Consider preserving the operator during round‑trip.🐛 Proposed fix to keep the operator round‑trippable
-function parseVariableValue(value: string): { - useRemoteVariable: boolean; - remoteVariableName: string; - defaultValue: string; -} { - // Match: ${VARIABLE_NAME:-default_value} or ${VARIABLE_NAME:=default_value} - const matchWithFallback = value.match(/^\$\{([A-Z_][A-Z0-9_]*):[-=](.*)\}$/); +function parseVariableValue(value: string): { + useRemoteVariable: boolean; + remoteVariableName: string; + defaultValue: string; + fallbackOperator: ':-' | ':='; +} { + // Match: ${VARIABLE_NAME:-default_value} or ${VARIABLE_NAME:=default_value} + const matchWithFallback = value.match(/^\$\{([A-Z_][A-Z0-9_]*):([-=])(.*)\}$/); if (matchWithFallback) { return { useRemoteVariable: true, - remoteVariableName: matchWithFallback[1], - defaultValue: matchWithFallback[2] + remoteVariableName: matchWithFallback[1], + fallbackOperator: matchWithFallback[2] === '=' ? ':=' : ':-', + defaultValue: matchWithFallback[3] }; } @@ if (matchNoFallback) { return { useRemoteVariable: true, remoteVariableName: matchNoFallback[1], + fallbackOperator: ':-', defaultValue: '' }; } @@ return { useRemoteVariable: false, remoteVariableName: '', + fallbackOperator: ':-', defaultValue: value }; }-const [defaultValue, setDefaultValue] = React.useState(parsed.defaultValue); +const [defaultValue, setDefaultValue] = React.useState(parsed.defaultValue); +const [fallbackOperator] = React.useState(parsed.fallbackOperator); @@ - ? `\${${remoteVariableName}${defaultValue ? `:-${defaultValue}` : ''}}` + ? `\${${remoteVariableName}${defaultValue ? `${fallbackOperator}${defaultValue}` : ''}}`Also applies to: 115-118, 129-132
🤖 Fix all issues with AI agents
In `@sources/-session/SessionView.tsx`:
- Around line 283-284: Remove the unsafe "as any" casts on modelMode and
updateModelMode passed to AgentInput: locate the AgentInput usage where
modelMode={modelMode as any} and onModelModeChange={updateModelMode as any},
then either (A) widen the signature of updateModelMode to accept the full
ModelMode union (import ModelMode and change the function/type to (mode:
ModelMode) => void) so it matches AgentInput's onModelModeChange, or (B) keep
the narrower Gemini-only handler but create a typed wrapper that implements
(mode: ModelMode) => void, validates/guards that the incoming mode is one of
'default'|'gemini-2.5-pro'|'gemini-2.5-flash'|'gemini-2.5-flash-lite' before
calling updateModelMode (and document this behavior); remove the casts and
ensure imports reference the ModelMode type and AgentInput's prop types.
In `@sources/app/`(app)/new/pick/path.tsx:
- Line 4: Remove usage of CommonActions and useNavigation from
`@react-navigation/native` in PathPickerScreen and instead use the expo-router
navigation pattern to pass the selected path back via query params (or via
shared state) when navigating back; specifically replace any logic that sets
params on the previous route with a redirect/navigation call that includes the
selected path in the route query, and update callers to read from query params.
Also replace hardcoded user-visible strings ("Select Path", "No machine
selected", "Search paths...", and any other literals) with t('...') keys
consistent with existing i18n usage (e.g. t('common.selectPath') etc.). Finally,
export the page component wrapped in React.memo by changing export default
function PathPickerScreen() to export default React.memo(function
PathPickerScreen() { ... }) so the component is memoized. Ensure references to
CommonActions and useNavigation are removed from imports and implementation.
In `@sources/app/`(app)/new/pick/profile-edit.tsx:
- Line 4: Replace the direct import of useNavigation and CommonActions from
`@react-navigation/native` with expo-router's navigation API: remove CommonActions
and import useNavigation from 'expo-router' instead, then update any usages of
CommonActions.setParams or navigation.setParams to use the router/setParams
pattern provided by expo-router (e.g., useNavigation() from expo-router and
router.setParams for current route), and if you truly need cross-route
CommonActions.setParams behavior only then add react-navigation as an explicit
dependency; ensure any beforeRemove listener logic stays attached via the
expo-router useNavigation/router APIs.
In `@sources/app/`(app)/new/pick/profile.tsx:
- Around line 19-309: The ProfilePickerScreen page is missing a memo wrapper;
update the component export to use React.memo to prevent unnecessary re-renders
by importing memo (or using React.memo) and exporting the memoized component
(e.g., replace the current default export of ProfilePickerScreen with export
default memo(ProfilePickerScreen) and ensure memo is imported or referenced as
React.memo at the top of the file).
In `@sources/app/`(app)/settings/profiles.tsx:
- Around line 45-67: The early return when useProfiles is false causes hooks
(the useCallback calls around lines with setUseProfiles and any useMemo calls
later) to be invoked conditionally; move all hook invocations (the useCallback
handlers and useMemo computations referenced in this file) above the conditional
return so that useCallback, useMemo and any other hooks are always called in the
same order, then render the conditional JSX based on useProfiles (keep the
ItemList/ItemGroup return as-is but after the hooks have been declared).
In `@sources/components/AgentInput.tsx`:
- Around line 412-433: The Gemini permission modes are inconsistent between the
chip/keyboard cycling and overlay logic; create a single shared mapping object
(e.g., GEMINI_PERMISSION_MODES) and a helper accessor (e.g.,
getPermissionLabel(mode, provider)) and replace the inline ternary logic inside
the permissionChipLabel React.useMemo and the overlay/badge and keyboard-cycling
code paths to use that shared mapping; ensure the helper maps Claude-style keys
(acceptEdits, plan, bypassPermissions) to the correct labels and also handles
default, and update references to props.permissionMode, isGemini, and isCodex to
call the helper so all three locations (chip, overlay, keyboard cycling) use the
same unified mapping.
In `@sources/components/EnvironmentVariableCard.tsx`:
- Around line 198-369: The UI text in EnvironmentVariableCard is hardcoded;
replace all visible strings (e.g., 'Fallback value:', 'Value:', placeholder
texts, "Secret value - not retrieved for security", "Overriding documented
default: {expectedValue}", "Use value from machine environment", "Resolved when
the session starts on the selected machine.", "Source variable", placeholder
"Source variable name (e.g., Z_AI_MODEL)", status messages like "Checking
{machineLabel}...", "Empty on {machineLabel} (using fallback)", "Not found on
{machineLabel}", "Value found on {machineLabel}", "Differs from documented
value: {expectedValue}", and "Session will receive: {variable.name} =
{resolvedSessionValue}") to use the i18n t(...) helper instead; update
EnvironmentVariableCard to call t('key') or t('key', { expectedValue,
machineLabel, variableName, resolvedSessionValue }) where needed and add
matching keys to all locale files so translations exist for each new key.
In `@sources/components/ItemGroup.tsx`:
- Around line 141-152: The divider logic fails for items inside React.Fragment
because fragments are returned unchanged and their children aren't considered
when computing isLast; update the mapping in the component that iterates over
children (the React.Children.map block that uses
React.isValidElement<ItemChildProps> and checks child.type === React.Fragment)
to first flatten or recursively collect selectable children (reuse the same
traversal approach as countSelectable) so you can compute the correct index/last
status across fragment boundaries, then clone each selectable child with
showDivider: !isLast && childProps.showDivider !== false; ensure fragments are
traversed rather than returned verbatim.
In `@sources/components/ItemRowActions.tsx`:
- Around line 35-42: The Modal.show call in ItemRowActions.tsx currently uses an
unsafe "as any" cast; remove that cast and fix the typing so Modal.show accepts
the ItemActionsMenuModal props without bypassing type checks—either (a) update
CustomModalConfig in sources/modal/types.ts to express that onClose is injected
by CustomModal.tsx (e.g., make onClose optional or modeled as injected), or (b)
add a typed helper/wrapper around Modal.show that accepts the component props
(title and actions) and returns the fully-typed config while guaranteeing
onClose will be provided at runtime; then call Modal.show with the correctly
typed config (using ItemActionsMenuModal, overflowActions and props.title)
instead of casting to any.
In `@sources/components/newSession/PathSelector.tsx`:
- Around line 230-447: Replace all hardcoded user-facing strings in
PathSelector's JSX with translation keys using t(...): update title props on
ItemGroup ("Enter Path", "Recent Paths", "Favorite Paths", "Suggested Paths",
"Paths"), placeholders ("Search paths...", "Enter path (e.g.
/home/user/projects)") passed to SearchHeader and MultiTextInput, and Item
titles/disabled messages ("No matches", "No recent paths", "No favorite paths",
"No suggested paths", "No paths") to use t('pathSelector.xxx') (or your chosen
key namespace); modify usages in components SearchHeader, MultiTextInput,
ItemGroup and Item (and any rightElement text) to call t(...) instead of
literals, then add those keys to all locale translation files so each locale
includes the new entries.
In `@sources/components/newSession/ProfileCompatibilityIcon.tsx`:
- Around line 33-62: ProfileCompatibilityIcon uses inline style objects that
should be moved into unistyles StyleSheet.create in function mode to enable
theme-based caching; create a styles factory via StyleSheet.create(() => ({
container, singleGlyphText, multiContainer, mappedTextBase })) and replace the
static parts of the inline styles for the outer View (container), the
single-glyph Text (singleGlyphText), the inner column View (multiContainer) and
the per-item Text base (mappedTextBase) with those style refs, while preserving
runtime-computed properties (fontSize, lineHeight) inline in the JSX; keep
references to theme.colors.textSecondary, Typography.default(), and the dynamic
values size, multiScale and item.factor when applying styles so only
static/theme-dependent parts move into the StyleSheet function.
In `@sources/text/translations/es.ts`:
- Around line 211-219: The listed translation keys (profiles, profilesEnabled,
profilesDisabled, pickerSearch, pickerSearchSubtitle, machinePickerSearch,
machinePickerSearchSubtitle, pathPickerSearch, pathPickerSearchSubtitle) are
still in English; update their string values in sources/text/translations/es.ts
to Spanish equivalents so all user-visible text is localized, replacing each
English value with the appropriate Spanish translation while preserving the keys
and file encoding.
In `@sources/text/translations/zh-Hans.ts`:
- Around line 213-221: The listed localization keys (profiles, profilesEnabled,
profilesDisabled, pickerSearch, pickerSearchSubtitle, machinePickerSearch,
machinePickerSearchSubtitle, pathPickerSearch, pathPickerSearchSubtitle) in
zh-Hans.ts are still in English; replace each English string value with the
appropriate Simplified Chinese translations so all user-visible text is
localized, ensuring you only change the string values for those keys and keep
the keys themselves intact and properly escaped if needed.
🟡 Minor comments (29)
sources/app/(app)/session/[id]/info.tsx-337-344 (1)
337-344: Missing i18n for "AI Profile" string.Line 339 uses a hardcoded string
"AI Profile"instead of using thet()function. Per coding guidelines, all user-visible strings must uset(...)for internationalization.🌐 Suggested fix
{useProfiles && session.metadata?.profileId !== undefined && ( <Item - title="AI Profile" + title={t('sessionInfo.aiProfile')} detail={profileLabel} icon={<Ionicons name="person-circle-outline" size={29} color="#5856D6" />} showChevron={false} /> )}Ensure the translation key
sessionInfo.aiProfileis added to the translation files.sources/components/tools/knownTools.tsx-594-603 (1)
594-603: Localize the change_title label.This is user-visible and should go through
t(...).♻️ Suggested change
- title: 'Change Title', + title: t('tools.names.changeTitle'),As per coding guidelines, ...
sources/components/tools/views/GeminiExecuteView.tsx-63-69 (1)
63-69: Localize the cwd label.The “📁 ” prefix is user-visible and should go through
t(...)(with a new key).♻️ Suggested change
import { CodeView } from '@/components/CodeView'; +import { t } from '@/text'; @@ - {cwd && ( - <Text style={styles.cwdText}>📁 {cwd}</Text> - )} + {cwd && ( + <Text style={styles.cwdText}>{t('tools.cwd', { path: cwd })}</Text> + )}As per coding guidelines, ...
sources/app/(app)/settings/profiles.tsx-178-178 (1)
178-178: Missing i18n for error messages.Error messages at lines 178, 194, and 206 use hardcoded strings. Use
t()for consistency.🌐 Suggested fix
- Modal.alert(t('common.error'), 'Enter a profile name.'); + Modal.alert(t('common.error'), t('profiles.errors.nameRequired')); - Modal.alert(t('common.error'), 'A profile with that name already exists.'); + Modal.alert(t('common.error'), t('profiles.errors.duplicateName'));Also applies to: 194-194, 206-206
sources/app/(app)/settings/profiles.tsx-235-268 (1)
235-268: Multiple hardcoded strings in profile actions.Profile action titles ("Favorites", "Your Profiles", "Built-in Profiles", "Add to favorites", "Edit profile", "Duplicate profile", "Delete profile", etc.) should use
t()function for internationalization.Also applies to: 302-334, 368-392
sources/components/SettingsView.tsx-40-41 (1)
40-41: Unused variableuseEnhancedSessionWizard.
useEnhancedSessionWizardis read from settings but never used in this component. Remove it to avoid confusion.🧹 Suggested fix
const experiments = useSetting('experiments'); - const useEnhancedSessionWizard = useSetting('useEnhancedSessionWizard'); const useProfiles = useSetting('useProfiles');sources/app/(app)/settings/profiles.tsx-97-99 (1)
97-99: Missing i18n for user-visible strings.The Modal.confirm dialog uses hardcoded English strings instead of
t()function. As per coding guidelines, always uset(...)for ALL user-visible strings.🌐 Suggested fix
const discard = await Modal.confirm( - 'Discard changes?', - 'You have unsaved changes. Discard them?', - { destructive: true, confirmText: 'Discard', cancelText: 'Keep editing' }, + t('profiles.discardChanges.title'), + t('profiles.discardChanges.message'), + { destructive: true, confirmText: t('profiles.discardChanges.confirm'), cancelText: t('profiles.discardChanges.cancel') }, );sources/components/SearchHeader.tsx-105-111 (1)
105-111: Show the clear icon for whitespace-only input.Using
value.trim().length > 0hides the clear affordance when the field contains only spaces, leaving no quick way to reset the input. Consider checkingvalue.length > 0so any non-empty value reveals the clear icon.💡 Suggested fix
- {value.trim().length > 0 && ( + {value.length > 0 && (sources/sync/messageMeta.ts-15-17 (1)
15-17: UsedisplayText !== undefinedto allow empty strings.
displayTextcurrently uses a truthiness check, which excludes empty strings. SincedisplayTextis typed asstring | undefined(not nullable), empty strings are valid values. Change the condition todisplayText !== undefinedto include them.- ...(params.displayText ? { displayText: params.displayText } : {}), + ...(params.displayText !== undefined ? { displayText: params.displayText } : {}),sources/app/(app)/new/pick/profile.tsx-45-51 (1)
45-51: Hardcoded 'Built-in' string should use i18n.The subtitle construction uses hardcoded "Built-in" text.
🌐 Suggested fix
const getProfileSubtitle = React.useCallback((profile: AIBackendProfile) => { const backend = getProfileBackendSubtitle(profile); if (profile.isBuiltIn) { - return backend ? `Built-in · ${backend}` : 'Built-in'; + return backend ? `${t('profiles.builtIn')} · ${backend}` : t('profiles.builtIn'); } return backend; }, [getProfileBackendSubtitle]);sources/app/(app)/new/pick/profile.tsx-129-188 (1)
129-188: Inconsistent indentation - appears to use tabs instead of 4 spaces.The
renderProfileRowRightElementcallback has inconsistent indentation that appears to mix tabs and spaces. As per coding guidelines, use 4 spaces for indentation.sources/app/(app)/new/pick/profile.tsx-130-155 (1)
130-155: Multiple hardcoded strings should use i18n.Several user-visible strings are hardcoded instead of using the
t()function. As per coding guidelines, always uset(...)for ALL user-visible strings.Hardcoded strings found:
- Line 133:
'Remove from favorites'/'Add to favorites'- Line 141:
'Edit profile'- Line 147:
'Duplicate profile'- Line 155:
'Delete profile'- Line 219:
"Favorites"- Line 241:
"Your AI Profiles"- Line 263:
"Built-in AI Profiles"🌐 Example fix for action titles
{ id: 'favorite', - title: isFavorite ? 'Remove from favorites' : 'Add to favorites', + title: isFavorite ? t('profiles.actions.removeFromFavorites') : t('profiles.actions.addToFavorites'), icon: isFavorite ? 'star' : 'star-outline', // ... }, { id: 'edit', - title: 'Edit profile', + title: t('profiles.actions.edit'), icon: 'create-outline', // ... },Also applies to: 219-219, 241-241, 263-263
sources/components/SearchableListSelector.tsx-316-316 (1)
316-316: Hardcoded strings should uset()for internationalization.The string
'No matches'appears in multiple places but is not wrapped with thet()function. Per coding guidelines, all user-visible strings must uset(...)for internationalization.Suggested fix
- ? renderEmptyRow(showNoMatches ? 'No matches' : config.noItemsMessage) + ? renderEmptyRow(showNoMatches ? t('common.noMatches') : config.noItemsMessage)Apply this pattern to all four occurrences (lines 316, 350, 365, 379). You'll also need to add the translation key to your i18n files.
Based on coding guidelines, always use
t(...)function for all user-visible strings.Also applies to: 350-350, 365-365, 379-379
sources/components/newSession/MachineSelector.tsx-37-41 (1)
37-41: Default prop values should uset()for internationalization.The hardcoded default strings for
searchPlaceholder,recentSectionTitle,favoritesSectionTitle,allSectionTitle, andnoItemsMessageshould use thet()function for proper internationalization support.Suggested fix
- searchPlaceholder = 'Type to filter machines...', - recentSectionTitle = 'Recent Machines', - favoritesSectionTitle = 'Favorite Machines', - allSectionTitle = 'All Machines', - noItemsMessage = 'No machines available', + searchPlaceholder = t('machineSelector.searchPlaceholder'), + recentSectionTitle = t('machineSelector.recentSectionTitle'), + favoritesSectionTitle = t('machineSelector.favoritesSectionTitle'), + allSectionTitle = t('machineSelector.allSectionTitle'), + noItemsMessage = t('machineSelector.noItemsMessage'),Note: You'll need to add the
timport and add these keys to your translation files.Based on coding guidelines, always use
t(...)function for all user-visible strings.sources/sync/storage.ts-121-121 (1)
121-121: Type safety issue:updateSessionModelModesignature excludes null butSession.modelModeallows it.The Gemini model variants (
gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite) are correctly and consistently defined acrossstorageTypes.tsandstorage.ts. However, the type signatures are inconsistent:
Session.modelModein storageTypes.ts (line 74) allows:'default' | 'gemini-2.5-pro' | 'gemini-2.5-flash' | 'gemini-2.5-flash-lite' | nullupdateSessionModelModein storage.ts (lines 121, 808) accepts:'default' | 'gemini-2.5-pro' | 'gemini-2.5-flash' | 'gemini-2.5-flash-lite'(no null)There is no API method to set
modelModetonull, yet the Session type permits it. Consider either: (1) adding a separate method likeclearSessionModelMode()if null-clearing is intended, or (2) removing| nullfrom the Session type if modelMode should never be explicitly cleared.sources/text/translations/ca.ts-211-219 (1)
211-219: Localize the new Catalan strings.These entries are still English (settings feature toggles and Gemini permission labels), which will produce a mixed-language UI in the Catalan locale.
Also applies to: 442-449
sources/text/translations/it.ts-240-248 (1)
240-248: Localize the new settings feature strings for Italian.These values are still English, which will surface a mixed-language UI in the Italian locale.
sources/components/newSession/PathSelector.tsx-62-62 (1)
62-62: Avoidanyfor the search input ref.The
searchInputRefis passed toSearchHeadercomponents which expectinputRef?: React.Ref<TextInput>. Use the concreteTextInputtype from React Native to maintain strict typing.✅ Suggested fix
-import { View, Pressable } from 'react-native'; +import { View, Pressable, TextInput } from 'react-native'; @@ -const searchInputRef = useRef<any>(null); +const searchInputRef = useRef<TextInput | null>(null);sources/sync/sync.ts-1513-1525 (1)
1513-1525: Guard lifecycle debug logging in production.The new
console.logtraces will run in production; consider gating with__DEV__or routing throughlog.logto avoid noisy logs.sources/components/ProfileEditForm.tsx-83-85 (1)
83-85: Localize new user-visible strings.Labels like “Preview Machine”, “Setup Instructions”, “Default Permission Mode”, Tmux helper text, and alert strings are hardcoded. Please wrap them with
t(...)so they’re localized. As per coding guidelines, ...Also applies to: 229-233, 250-253, 302-321, 346-380, 376-404, 412-417
sources/text/translations/es.ts-442-449 (1)
442-449: Gemini permission mode values should be translated to Spanish.While the title is correctly translated to Spanish, the permission mode labels remain in English.
🌐 Suggested translations
- readOnly: 'Read Only Mode', - safeYolo: 'Safe YOLO', - yolo: 'YOLO', - badgeReadOnly: 'Read Only Mode', - badgeSafeYolo: 'Safe YOLO', - badgeYolo: 'YOLO', + readOnly: 'Modo de solo lectura', + safeYolo: 'YOLO Seguro', + yolo: 'YOLO', + badgeReadOnly: 'Modo de solo lectura', + badgeSafeYolo: 'YOLO Seguro', + badgeYolo: 'YOLO',sources/text/translations/zh-Hans.ts-444-451 (1)
444-451: Gemini permission mode values should be translated to Chinese.The permission mode labels are left in English. While technical terms like "YOLO" might be kept as-is, the descriptive labels should be translated for consistency with the rest of the file.
🌐 Suggested translations
- readOnly: 'Read Only Mode', - safeYolo: 'Safe YOLO', - yolo: 'YOLO', - badgeReadOnly: 'Read Only Mode', - badgeSafeYolo: 'Safe YOLO', - badgeYolo: 'YOLO', + readOnly: '只读模式', + safeYolo: '安全 YOLO', + yolo: 'YOLO', + badgeReadOnly: '只读模式', + badgeSafeYolo: '安全 YOLO', + badgeYolo: 'YOLO',sources/components/newSession/EnvironmentVariablesPreviewModal.tsx-79-79 (1)
79-79: Hardcoded English strings should uset()for internationalization.Per coding guidelines, all user-visible strings should use the
t(...)function from@/text.🌐 Proposed fix
- const title = props.profileName ? `Env Vars · ${props.profileName}` : 'Environment Variables'; + const title = props.profileName + ? t('profiles.envVarsWithName', { name: props.profileName }) + : t('profiles.environmentVariables');- These environment variables are sent when starting the session. Values are resolved using the daemon on{' '} + {t('profiles.envVarsDescription')}{' '}- No environment variables are set for this profile. + {t('profiles.noEnvVars')}Also applies to: 142-142, 163-163
sources/app/(app)/new/pick/profile-edit.tsx-78-84 (1)
78-84: Hardcoded English strings should uset()for internationalization.Per coding guidelines, all user-visible strings should use the
t(...)function from@/text. These strings need to be added to translation files.🌐 Proposed fix using t() function
const confirmDiscard = React.useCallback(async () => { return Modal.confirm( - 'Discard changes?', - 'You have unsaved changes. Discard them?', - { destructive: true, confirmText: 'Discard', cancelText: 'Keep editing' }, + t('common.discardChanges'), + t('common.unsavedChangesWarning'), + { destructive: true, confirmText: t('common.discard'), cancelText: t('common.keepEditing') }, ); }, []);if (!savedProfile.name || savedProfile.name.trim() === '') { - Modal.alert(t('common.error'), 'Enter a profile name.'); + Modal.alert(t('common.error'), t('profiles.nameRequired')); return; }if (isDuplicateName) { - Modal.alert(t('common.error'), 'A profile with that name already exists.'); + Modal.alert(t('common.error'), t('profiles.duplicateName')); return; }Note: The translation key
profiles.nameRequiredalready exists. You'll need to addcommon.discardChanges,common.unsavedChangesWarning,common.keepEditing, andprofiles.duplicateNameto all translation files.Also applies to: 105-108, 127-130
sources/text/translations/ru.ts-193-201 (1)
193-201: Untranslated strings in Russian locale file.These strings are in English but should be translated to Russian for consistency with the rest of the file. Based on learnings, all user-visible strings should be localized.
🌐 Proposed Russian translations
- profiles: 'AI Profiles', - profilesEnabled: 'Profile selection enabled', - profilesDisabled: 'Profile selection disabled', - pickerSearch: 'Picker Search', - pickerSearchSubtitle: 'Show a search field in machine and path pickers', - machinePickerSearch: 'Machine search', - machinePickerSearchSubtitle: 'Show a search field in machine pickers', - pathPickerSearch: 'Path search', - pathPickerSearchSubtitle: 'Show a search field in path pickers', + profiles: 'Профили ИИ', + profilesEnabled: 'Выбор профиля включён', + profilesDisabled: 'Выбор профиля отключён', + pickerSearch: 'Поиск в селекторе', + pickerSearchSubtitle: 'Показывать поле поиска в селекторах машин и путей', + machinePickerSearch: 'Поиск машин', + machinePickerSearchSubtitle: 'Показывать поле поиска в селекторах машин', + pathPickerSearch: 'Поиск путей', + pathPickerSearchSubtitle: 'Показывать поле поиска в селекторах путей',sources/text/translations/pt.ts-211-219 (1)
211-219: Untranslated strings in Portuguese locale file.These strings are in English but should be translated to Portuguese for consistency. Based on learnings, all user-visible strings should be localized.
🌐 Proposed Portuguese translations
- profiles: 'AI Profiles', - profilesEnabled: 'Profile selection enabled', - profilesDisabled: 'Profile selection disabled', - pickerSearch: 'Picker Search', - pickerSearchSubtitle: 'Show a search field in machine and path pickers', - machinePickerSearch: 'Machine search', - machinePickerSearchSubtitle: 'Show a search field in machine pickers', - pathPickerSearch: 'Path search', - pathPickerSearchSubtitle: 'Show a search field in path pickers', + profiles: 'Perfis de IA', + profilesEnabled: 'Seleção de perfil ativada', + profilesDisabled: 'Seleção de perfil desativada', + pickerSearch: 'Busca no seletor', + pickerSearchSubtitle: 'Mostrar campo de busca nos seletores de máquinas e caminhos', + machinePickerSearch: 'Busca de máquinas', + machinePickerSearchSubtitle: 'Mostrar campo de busca nos seletores de máquinas', + pathPickerSearch: 'Busca de caminhos', + pathPickerSearchSubtitle: 'Mostrar campo de busca nos seletores de caminhos',sources/text/translations/pl.ts-222-230 (1)
222-230: Translate the new Settings > Features labels in Polish.
These newsettingsFeaturesstrings are still English, which will render mixed-language UI for Polish users. Please localize them.sources/text/translations/ja.ts-243-251 (1)
243-251: Localize the new Settings > Features labels in Japanese.
The newsettingsFeaturesstrings are still English, which will show mixed-language UI for Japanese users. Please translate them.sources/components/EnvironmentVariablesList.tsx-116-287 (1)
116-287: Localize the new Environment Variables UI strings.
Several user-visible labels/placeholders/alerts are hardcoded. Please route them throught(...)and add the keys to translations to avoid mixed-language UI.🌐 Suggested pattern (apply similarly to all literals)
- Modal.alert(t('common.error'), 'Enter a variable name.'); + Modal.alert(t('common.error'), t('profiles.envVars.errors.nameRequired')); - <Text style={...}>Environment Variables</Text> + <Text style={...}>{t('profiles.envVars.title')}</Text> - title={showAddForm ? 'Cancel' : 'Add Variable'} + title={showAddForm ? t('common.cancel') : t('profiles.envVars.addVariable')} - placeholder="Variable name (e.g., MY_CUSTOM_VAR)" + placeholder={t('profiles.envVars.namePlaceholder')} - <Text style={...}>Add</Text> + <Text style={...}>{t('profiles.envVars.addButton')}</Text>As per coding guidelines, all user-visible strings must use
t(...).
| modelMode={modelMode as any} | ||
| onModelModeChange={updateModelMode as any} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine SessionView.tsx around the mentioned lines
head -n 300 sources/-session/SessionView.tsx | tail -n 140Repository: leeroybrun/happy
Length of output: 5956
🏁 Script executed:
# Find AgentInput component to see its prop types
fd -n "AgentInput" -t f | head -20Repository: leeroybrun/happy
Length of output: 290
🏁 Script executed:
# Check what files exist in sources/components
ls -la sources/components/ | grep -i agent || echo "No agent files found, searching further"Repository: leeroybrun/happy
Length of output: 431
🏁 Script executed:
# Search for AgentInput more broadly
rg -l "AgentInput" sources/ --type ts --type tsxRepository: leeroybrun/happy
Length of output: 87
🏁 Script executed:
# Search for AgentInput without type restriction
rg "AgentInput" sources/ --include "*.ts" --include "*.tsx"Repository: leeroybrun/happy
Length of output: 501
🏁 Script executed:
# Read AgentInput.tsx to check the prop types
head -n 100 sources/components/AgentInput.tsxRepository: leeroybrun/happy
Length of output: 3760
🏁 Script executed:
# Search for modelMode prop definition in AgentInput
rg "modelMode" sources/components/AgentInput.tsx -B2 -A2Repository: leeroybrun/happy
Length of output: 539
🏁 Script executed:
# Run typecheck
cd sources && yarn typecheck 2>&1 || trueRepository: leeroybrun/happy
Length of output: 245
🏁 Script executed:
# Find permissionTypes to see the actual ModelMode definition
rg "type ModelMode" sources/Repository: leeroybrun/happy
Length of output: 115
🏁 Script executed:
# Also search for ModelMode definition more broadly
rg "ModelMode\s*=" sources/ -A2Repository: leeroybrun/happy
Length of output: 775
🏁 Script executed:
# Check the permissionTypes file
fd "permissionTypes" sources/ -t fRepository: leeroybrun/happy
Length of output: 91
🏁 Script executed:
# Read the full permissionTypes.ts file to see complete ModelMode definition
cat sources/sync/permissionTypes.tsRepository: leeroybrun/happy
Length of output: 539
🏁 Script executed:
# Also check if there's a proper ModelMode type being imported anywhere
rg "import.*ModelMode" sources/ -A1 -B1Repository: leeroybrun/happy
Length of output: 919
🏁 Script executed:
# Check the exact line where modelMode is passed to AgentInput in SessionView
sed -n '280,290p' sources/-session/SessionView.tsxRepository: leeroybrun/happy
Length of output: 568
🏁 Script executed:
# Check if SessionView.tsx imports ModelMode type
rg "import.*ModelMode" sources/-session/SessionView.tsxRepository: leeroybrun/happy
Length of output: 42
🏁 Script executed:
# Check the derivation of modelMode at line 171-173
sed -n '171,175p' sources/-session/SessionView.tsxRepository: leeroybrun/happy
Length of output: 435
🏁 Script executed:
# Check if updateSessionModelMode function exists and what it accepts
rg "updateSessionModelMode" sources/ -A3 -B1Repository: leeroybrun/happy
Length of output: 1943
🏁 Script executed:
# Check what types are actually being used in SessionView
head -n 50 sources/-session/SessionView.tsx | grep -E "^import|^from"Repository: leeroybrun/happy
Length of output: 1934
🏁 Script executed:
# Check the session object type to see what modelMode property type is
rg "session.*modelMode" sources/ -B2 -A2 | head -30Repository: leeroybrun/happy
Length of output: 1918
🏁 Script executed:
# Check the Session type definition to see what modelMode property is typed as
rg "interface.*Session\|type.*Session" sources/sync/storageTypes.ts -A30 | head -60Repository: leeroybrun/happy
Length of output: 42
🏁 Script executed:
# Check if there's a Session interface that defines modelMode property
rg "modelMode.*:" sources/sync/storageTypes.ts -B2 -A2Repository: leeroybrun/happy
Length of output: 627
Remove as any casts—they hide type incompatibility between the callback signature and AgentInput's expectations.
The modelMode derivation is correct, but updateModelMode has a deliberate type restriction: it only accepts 'default' | 'gemini-2.5-pro' | 'gemini-2.5-flash' | 'gemini-2.5-flash-lite'. However, AgentInput expects onModelModeChange to accept the full ModelMode union (which includes sonnet, opus, gpt-5-*, adaptiveUsage, etc.).
The as any casts bypass TypeScript's type checking and violate the coding guideline requiring strict mode and proper typing. If AgentInput attempts to call the callback with a model mode outside the restricted set, the mismatch will only surface at runtime.
Either import and use the ModelMode type properly, or align the callback signature with AgentInput's expectations. If the restriction is intentional for Gemini-only sessions, document the design decision rather than hiding it with as any.
🤖 Prompt for AI Agents
In `@sources/-session/SessionView.tsx` around lines 283 - 284, Remove the unsafe
"as any" casts on modelMode and updateModelMode passed to AgentInput: locate the
AgentInput usage where modelMode={modelMode as any} and
onModelModeChange={updateModelMode as any}, then either (A) widen the signature
of updateModelMode to accept the full ModelMode union (import ModelMode and
change the function/type to (mode: ModelMode) => void) so it matches
AgentInput's onModelModeChange, or (B) keep the narrower Gemini-only handler but
create a typed wrapper that implements (mode: ModelMode) => void,
validates/guards that the incoming mode is one of
'default'|'gemini-2.5-pro'|'gemini-2.5-flash'|'gemini-2.5-flash-lite' before
calling updateModelMode (and document this behavior); remove the casts and
ensure imports reference the ModelMode type and AgentInput's prop types.
| import { View, Text, ScrollView, Pressable } from 'react-native'; | ||
| import { View, Text, Pressable } from 'react-native'; | ||
| import { Stack, useRouter, useLocalSearchParams } from 'expo-router'; | ||
| import { CommonActions, useNavigation } from '@react-navigation/native'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the specific file to understand the context
cat -n sources/app/\(app\)/new/pick/path.tsx | head -200Repository: leeroybrun/happy
Length of output: 9112
🏁 Script executed:
# Search for CommonActions and setParams usage across the codebase
rg -n "CommonActions|setParams" --type=ts --type=tsx -g '!node_modules' -A 3Repository: leeroybrun/happy
Length of output: 87
🏁 Script executed:
# Check if the file is wrapped in memo
rg -n "export.*memo|memo\(" sources/app/\(app\)/new/pick/path.tsxRepository: leeroybrun/happy
Length of output: 42
🏁 Script executed:
# Search for CommonActions and setParams usage with correct flags
rg "CommonActions|setParams" --type ts --type tsx -g '!node_modules' -A 3 -B 1Repository: leeroybrun/happy
Length of output: 87
🏁 Script executed:
# Search for expo-router param passing patterns - specifically router.back() usage
rg "router\.back\(\)" --type ts --type tsx -B 5 -A 2 | head -80Repository: leeroybrun/happy
Length of output: 87
🏁 Script executed:
# Check if other pages in the app/(app) directory are wrapped in memo
rg "export default.*memo|memo\(.*function" sources/app/\(app\)/ --type ts --type tsxRepository: leeroybrun/happy
Length of output: 87
🏁 Script executed:
# Search for the "new/index.tsx" file that receives the path params to understand the pattern
fd "new/index.tsx" --type fRepository: leeroybrun/happy
Length of output: 42
🏁 Script executed:
# Search for CommonActions usage across all TypeScript/TSX files using glob
rg "CommonActions|setParams" "**/*.ts" "**/*.tsx" -A 3 -B 1 | head -100Repository: leeroybrun/happy
Length of output: 156
🏁 Script executed:
# Look for the new/index.tsx receiver file
find sources/app -name "index.tsx" | grep newRepository: leeroybrun/happy
Length of output: 91
🏁 Script executed:
# Check other page exports to see if memo is commonly used
rg "export default" sources/app/\(app\)/ -A 1 | head -40Repository: leeroybrun/happy
Length of output: 2176
🏁 Script executed:
# Search for t() usage to understand translation pattern
rg "t\('.*'" sources/app/\(app\)/new/pick/path.tsx | head -20Repository: leeroybrun/happy
Length of output: 173
🏁 Script executed:
cat -n sources/app/\(app\)/new/index.tsx | head -150Repository: leeroybrun/happy
Length of output: 7297
🏁 Script executed:
# Search for any router.setParams or equivalent expo-router patterns
rg "router\." sources/app/\(app\)/ -A 2 | head -60Repository: leeroybrun/happy
Length of output: 3160
🏁 Script executed:
rg "useLocalSearchParams|path" sources/app/\(app\)/new/index.tsx -B 2 -A 2 | head -80Repository: leeroybrun/happy
Length of output: 2992
🏁 Script executed:
# Check if useNavigation is used anywhere else in the app/(app) pages
rg "useNavigation\|from '@react-navigation" sources/app/\(app\)/ | head -30Repository: leeroybrun/happy
Length of output: 42
🏁 Script executed:
# Search for all hardcoded user-visible strings in the path.tsx file
rg "headerTitle|placeholder|emptyText|'[A-Z]" sources/app/\(app\)/new/pick/path.tsx -n | head -30Repository: leeroybrun/happy
Length of output: 331
Remove react-navigation imports and use expo-router for passing params to previous route.
The file imports CommonActions and useNavigation from @react-navigation/native to set params on the previous route. This violates the guideline to always use expo-router API. Instead, pass the selected path through query parameters when navigating back, or use a state management solution. The pattern should align with how other pages in the app handle navigation.
Additionally:
- Hardcoded strings: Lines 137, 172 ("Select Path"), 160 ("No machine selected"), and 197 ("Search paths...") are not wrapped with
t()for internationalization. The file already usest('common.back')elsewhere, so apply the same pattern to all user-visible strings. - Wrap component in memo: Per guidelines, page components in
sources/app/**/*.tsxmust be wrapped inReact.memo(). Changeexport default function PathPickerScreen()toexport default React.memo(function PathPickerScreen() { ... }).
🤖 Prompt for AI Agents
In `@sources/app/`(app)/new/pick/path.tsx at line 4, Remove usage of CommonActions
and useNavigation from `@react-navigation/native` in PathPickerScreen and instead
use the expo-router navigation pattern to pass the selected path back via query
params (or via shared state) when navigating back; specifically replace any
logic that sets params on the previous route with a redirect/navigation call
that includes the selected path in the route query, and update callers to read
from query params. Also replace hardcoded user-visible strings ("Select Path",
"No machine selected", "Search paths...", and any other literals) with t('...')
keys consistent with existing i18n usage (e.g. t('common.selectPath') etc.).
Finally, export the page component wrapped in React.memo by changing export
default function PathPickerScreen() to export default React.memo(function
PathPickerScreen() { ... }) so the component is memoized. Ensure references to
CommonActions and useNavigation are removed from imports and implementation.
| import React from 'react'; | ||
| import { View, KeyboardAvoidingView, Platform, useWindowDimensions } from 'react-native'; | ||
| import { Stack, useRouter, useLocalSearchParams } from 'expo-router'; | ||
| import { CommonActions, useNavigation } from '@react-navigation/native'; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
expo-router beforeRemove listener navigation lifecycle
💡 Result:
Short summary — expo-router uses React Navigation under the hood, so the beforeRemove lifecycle behaves like React Navigation’s beforeRemove event:
- What it is: emitted when a screen is about to be removed from the navigation state; listener can call e.preventDefault() to stop the removal. [1][2]
- How to attach: use the navigation object from expo-router’s useNavigation (or use React Navigation hooks) and add navigation.addListener('beforeRemove', callback) — or use React Navigation’s convenience hook usePreventRemove. [3][1]
- Key limitations / gotchas:
- Triggers only when a screen is being removed from the navigator state (back, pop, swipe-back, reset). It does NOT fire for simple unfocus (pushing a screen on top, switching tabs) or when the app is closed by the OS. [3]
- Edge cases: behavior for the last remaining screen or other navigator-specific cases may vary (see React Navigation issues). [2]
- Recommendation: use expo-router’s useNavigation to get the navigation instance, then add the beforeRemove listener or use usePreventRemove when you need to block/back-press confirmation. [1][3]
References:
- Expo Router useNavigation docs. [1]
- React Navigation — navigation events (beforeRemove) and related issues. [2]
- React Navigation — usePreventRemove (limitations & guidance). [3]
🌐 Web query:
expo-router setParams CommonActions equivalent parameter passing
💡 Result:
Short answer: use expo-router's router.setParams to update the current route's params (or pass params when navigating). You can also dispatch React Navigation's CommonActions.setParams if you need its source/target behavior.
Examples:
-
Update params on the focused route:
- import { router } from 'expo-router'
- router.setParams({ foo: 'bar' }). [1]
-
Navigate / pass params to another route:
- router.push({ pathname: '/user/[id]', params: { id: '123', q: 'x' } }) or <Link href={{ pathname: '/user/[id]', params: { id: '123' } }} />. Read them with useLocalSearchParams / useSearchParams. [1]
-
If you need React Navigation CommonActions (e.g., set params for a specific route key or use replaceParams), get navigation via useNavigation and dispatch:
- navigation.dispatch(CommonActions.setParams({ foo: 'bar' }));. [2][3]
Sources: Expo Router docs (navigation / setParams), React Navigation CommonActions docs, and Expo migration guide. [1][2][3]
🏁 Script executed:
# Let me examine the actual file to understand what functionality is being used
cat -n sources/app/\(app\)/new/pick/profile-edit.tsx | head -100Repository: leeroybrun/happy
Length of output: 5060
🏁 Script executed:
cat -n sources/app/\(app\)/new/pick/profile-edit.tsx | grep -A5 -B5 "CommonActions\|useNavigation"Repository: leeroybrun/happy
Length of output: 1759
Remove direct @react-navigation/native import; use expo-router's useNavigation instead.
The coding guidelines require using expo-router API exclusively. Import useNavigation from expo-router on line 3 rather than from @react-navigation/native. expo-router provides full equivalents for both the beforeRemove listener and parameter handling functionality you're using. If you require CommonActions.setParams with route-specific source targeting, import it from react-navigation as an explicit dependency only if no expo-router alternative suffices, but otherwise migrate to router.setParams() for the current route or refactor the cross-route parameter passing pattern to follow expo-router conventions.
🤖 Prompt for AI Agents
In `@sources/app/`(app)/new/pick/profile-edit.tsx at line 4, Replace the direct
import of useNavigation and CommonActions from `@react-navigation/native` with
expo-router's navigation API: remove CommonActions and import useNavigation from
'expo-router' instead, then update any usages of CommonActions.setParams or
navigation.setParams to use the router/setParams pattern provided by expo-router
(e.g., useNavigation() from expo-router and router.setParams for current route),
and if you truly need cross-route CommonActions.setParams behavior only then add
react-navigation as an explicit dependency; ensure any beforeRemove listener
logic stays attached via the expo-router useNavigation/router APIs.
| export default function ProfilePickerScreen() { | ||
| const { theme } = useUnistyles(); | ||
| const router = useRouter(); | ||
| const navigation = useNavigation(); | ||
| const params = useLocalSearchParams<{ selectedId?: string; machineId?: string; profileId?: string | string[] }>(); | ||
| const useProfiles = useSetting('useProfiles'); | ||
| const experimentsEnabled = useSetting('experiments'); | ||
| const [profiles, setProfiles] = useSettingMutable('profiles'); | ||
| const [favoriteProfileIds, setFavoriteProfileIds] = useSettingMutable('favoriteProfiles'); | ||
|
|
||
| const selectedId = typeof params.selectedId === 'string' ? params.selectedId : ''; | ||
| const machineId = typeof params.machineId === 'string' ? params.machineId : undefined; | ||
| const profileId = Array.isArray(params.profileId) ? params.profileId[0] : params.profileId; | ||
|
|
||
| const renderProfileIcon = React.useCallback((profile: AIBackendProfile) => { | ||
| return <ProfileCompatibilityIcon profile={profile} />; | ||
| }, []); | ||
|
|
||
| const getProfileBackendSubtitle = React.useCallback((profile: Pick<AIBackendProfile, 'compatibility'>) => { | ||
| const parts: string[] = []; | ||
| if (profile.compatibility?.claude) parts.push(t('agentInput.agent.claude')); | ||
| if (profile.compatibility?.codex) parts.push(t('agentInput.agent.codex')); | ||
| if (experimentsEnabled && profile.compatibility?.gemini) parts.push(t('agentInput.agent.gemini')); | ||
| return parts.length > 0 ? parts.join(' • ') : ''; | ||
| }, [experimentsEnabled]); | ||
|
|
||
| const getProfileSubtitle = React.useCallback((profile: AIBackendProfile) => { | ||
| const backend = getProfileBackendSubtitle(profile); | ||
| if (profile.isBuiltIn) { | ||
| return backend ? `Built-in · ${backend}` : 'Built-in'; | ||
| } | ||
| return backend; | ||
| }, [getProfileBackendSubtitle]); | ||
|
|
||
| const setProfileParamAndClose = React.useCallback((profileId: string) => { | ||
| const state = navigation.getState(); | ||
| const previousRoute = state?.routes?.[state.index - 1]; | ||
| if (state && state.index > 0 && previousRoute) { | ||
| navigation.dispatch({ | ||
| ...CommonActions.setParams({ profileId }), | ||
| source: previousRoute.key, | ||
| } as never); | ||
| } | ||
| router.back(); | ||
| }, [navigation, router]); | ||
|
|
||
| React.useEffect(() => { | ||
| if (typeof profileId === 'string' && profileId.length > 0) { | ||
| setProfileParamAndClose(profileId); | ||
| } | ||
| }, [profileId, setProfileParamAndClose]); | ||
|
|
||
| const openProfileCreate = React.useCallback(() => { | ||
| const base = '/new/pick/profile-edit'; | ||
| router.push(machineId ? `${base}?machineId=${encodeURIComponent(machineId)}` as any : base as any); | ||
| }, [machineId, router]); | ||
|
|
||
| const openProfileEdit = React.useCallback((profileId: string) => { | ||
| const base = `/new/pick/profile-edit?profileId=${encodeURIComponent(profileId)}`; | ||
| router.push(machineId ? `${base}&machineId=${encodeURIComponent(machineId)}` as any : base as any); | ||
| }, [machineId, router]); | ||
|
|
||
| const openProfileDuplicate = React.useCallback((cloneFromProfileId: string) => { | ||
| const base = `/new/pick/profile-edit?cloneFromProfileId=${encodeURIComponent(cloneFromProfileId)}`; | ||
| router.push(machineId ? `${base}&machineId=${encodeURIComponent(machineId)}` as any : base as any); | ||
| }, [machineId, router]); | ||
|
|
||
| const { | ||
| favoriteProfiles: favoriteProfileItems, | ||
| customProfiles: nonFavoriteCustomProfiles, | ||
| builtInProfiles: nonFavoriteBuiltInProfiles, | ||
| favoriteIds: favoriteProfileIdSet, | ||
| } = React.useMemo(() => { | ||
| return buildProfileGroups({ customProfiles: profiles, favoriteProfileIds }); | ||
| }, [favoriteProfileIds, profiles]); | ||
|
|
||
| const toggleFavoriteProfile = React.useCallback((profileId: string) => { | ||
| if (favoriteProfileIdSet.has(profileId)) { | ||
| setFavoriteProfileIds(favoriteProfileIds.filter((id) => id !== profileId)); | ||
| } else { | ||
| setFavoriteProfileIds([profileId, ...favoriteProfileIds]); | ||
| } | ||
| }, [favoriteProfileIdSet, favoriteProfileIds, setFavoriteProfileIds]); | ||
|
|
||
| const handleAddProfile = React.useCallback(() => { | ||
| openProfileCreate(); | ||
| }, [openProfileCreate]); | ||
|
|
||
| const handleDeleteProfile = React.useCallback((profile: AIBackendProfile) => { | ||
| Modal.alert( | ||
| t('profiles.delete.title'), | ||
| t('profiles.delete.message', { name: profile.name }), | ||
| [ | ||
| { text: t('profiles.delete.cancel'), style: 'cancel' }, | ||
| { | ||
| text: t('profiles.delete.confirm'), | ||
| style: 'destructive', | ||
| onPress: () => { | ||
| // Only custom profiles live in `profiles` setting. | ||
| const updatedProfiles = profiles.filter(p => p.id !== profile.id); | ||
| setProfiles(updatedProfiles); | ||
| if (selectedId === profile.id) { | ||
| setProfileParamAndClose(''); | ||
| } | ||
| }, | ||
| }, | ||
| ], | ||
| ); | ||
| }, [profiles, selectedId, setProfileParamAndClose, setProfiles]); | ||
|
|
||
| const renderProfileRowRightElement = React.useCallback((profile: AIBackendProfile, isSelected: boolean, isFavorite: boolean) => { | ||
| const actions: ItemAction[] = [ | ||
| { | ||
| id: 'favorite', | ||
| title: isFavorite ? 'Remove from favorites' : 'Add to favorites', | ||
| icon: isFavorite ? 'star' : 'star-outline', | ||
| color: isFavorite ? theme.colors.button.primary.background : theme.colors.textSecondary, | ||
| onPress: () => toggleFavoriteProfile(profile.id), | ||
| }, | ||
| { | ||
| id: 'edit', | ||
| title: 'Edit profile', | ||
| icon: 'create-outline', | ||
| onPress: () => openProfileEdit(profile.id), | ||
| }, | ||
| { | ||
| id: 'copy', | ||
| title: 'Duplicate profile', | ||
| icon: 'copy-outline', | ||
| onPress: () => openProfileDuplicate(profile.id), | ||
| }, | ||
| ]; | ||
| if (!profile.isBuiltIn) { | ||
| actions.push({ | ||
| id: 'delete', | ||
| title: 'Delete profile', | ||
| icon: 'trash-outline', | ||
| destructive: true, | ||
| onPress: () => handleDeleteProfile(profile), | ||
| }); | ||
| } | ||
|
|
||
| return ( | ||
| <View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}> | ||
| <View style={{ width: 24, alignItems: 'center', justifyContent: 'center' }}> | ||
| <Ionicons | ||
| name="checkmark-circle" | ||
| size={24} | ||
| color={theme.colors.button.primary.background} | ||
| style={{ opacity: isSelected ? 1 : 0 }} | ||
| /> | ||
| </View> | ||
| <ItemRowActions | ||
| title={profile.name} | ||
| actions={actions} | ||
| compactActionIds={['edit']} | ||
| iconSize={20} | ||
| /> | ||
| </View> | ||
| ); | ||
| }, [ | ||
| handleDeleteProfile, | ||
| openProfileEdit, | ||
| openProfileDuplicate, | ||
| theme.colors.button.primary.background, | ||
| theme.colors.button.secondary.tint, | ||
| theme.colors.deleteAction, | ||
| theme.colors.textSecondary, | ||
| toggleFavoriteProfile, | ||
| ]); | ||
|
|
||
| return ( | ||
| <> | ||
| <Stack.Screen | ||
| options={{ | ||
| headerShown: true, | ||
| headerTitle: t('profiles.title'), | ||
| headerBackTitle: t('common.back'), | ||
| }} | ||
| /> | ||
|
|
||
| <ItemList style={{ paddingTop: 0 }}> | ||
| {!useProfiles ? ( | ||
| <ItemGroup footer={t('settingsFeatures.profilesDisabled')}> | ||
| <Item | ||
| title={t('settingsFeatures.profiles')} | ||
| subtitle={t('settingsFeatures.profilesDisabled')} | ||
| icon={<Ionicons name="person-outline" size={29} color={theme.colors.textSecondary} />} | ||
| showChevron={false} | ||
| /> | ||
| <Item | ||
| title={t('settings.featuresTitle')} | ||
| subtitle={t('settings.featuresSubtitle')} | ||
| icon={<Ionicons name="flask-outline" size={29} color={theme.colors.textSecondary} />} | ||
| onPress={() => router.push('/settings/features')} | ||
| /> | ||
| </ItemGroup> | ||
| ) : ( | ||
| <> | ||
| {favoriteProfileItems.length > 0 && ( | ||
| <ItemGroup title="Favorites"> | ||
| {favoriteProfileItems.map((profile, index) => { | ||
| const isSelected = selectedId === profile.id; | ||
| const isLast = index === favoriteProfileItems.length - 1; | ||
| return ( | ||
| <Item | ||
| key={profile.id} | ||
| title={profile.name} | ||
| subtitle={getProfileSubtitle(profile)} | ||
| icon={renderProfileIcon(profile)} | ||
| onPress={() => setProfileParamAndClose(profile.id)} | ||
| showChevron={false} | ||
| selected={isSelected} | ||
| rightElement={renderProfileRowRightElement(profile, isSelected, true)} | ||
| showDivider={!isLast} | ||
| /> | ||
| ); | ||
| })} | ||
| </ItemGroup> | ||
| )} | ||
|
|
||
| {nonFavoriteCustomProfiles.length > 0 && ( | ||
| <ItemGroup title="Your AI Profiles"> | ||
| {nonFavoriteCustomProfiles.map((profile, index) => { | ||
| const isSelected = selectedId === profile.id; | ||
| const isLast = index === nonFavoriteCustomProfiles.length - 1; | ||
| const isFavorite = favoriteProfileIdSet.has(profile.id); | ||
| return ( | ||
| <Item | ||
| key={profile.id} | ||
| title={profile.name} | ||
| subtitle={getProfileSubtitle(profile)} | ||
| icon={renderProfileIcon(profile)} | ||
| onPress={() => setProfileParamAndClose(profile.id)} | ||
| showChevron={false} | ||
| selected={isSelected} | ||
| rightElement={renderProfileRowRightElement(profile, isSelected, isFavorite)} | ||
| showDivider={!isLast} | ||
| /> | ||
| ); | ||
| })} | ||
| </ItemGroup> | ||
| )} | ||
|
|
||
| <ItemGroup title="Built-in AI Profiles"> | ||
| <Item | ||
| title={t('profiles.noProfile')} | ||
| subtitle={t('profiles.noProfileDescription')} | ||
| icon={<Ionicons name="home-outline" size={29} color={theme.colors.textSecondary} />} | ||
| onPress={() => setProfileParamAndClose('')} | ||
| showChevron={false} | ||
| selected={selectedId === ''} | ||
| rightElement={selectedId === '' | ||
| ? <Ionicons name="checkmark-circle" size={24} color={theme.colors.button.primary.background} /> | ||
| : null} | ||
| showDivider={nonFavoriteBuiltInProfiles.length > 0} | ||
| /> | ||
| {nonFavoriteBuiltInProfiles.map((profile, index) => { | ||
| const isSelected = selectedId === profile.id; | ||
| const isLast = index === nonFavoriteBuiltInProfiles.length - 1; | ||
| const isFavorite = favoriteProfileIdSet.has(profile.id); | ||
| return ( | ||
| <Item | ||
| key={profile.id} | ||
| title={profile.name} | ||
| subtitle={getProfileSubtitle(profile)} | ||
| icon={renderProfileIcon(profile)} | ||
| onPress={() => setProfileParamAndClose(profile.id)} | ||
| showChevron={false} | ||
| selected={isSelected} | ||
| rightElement={renderProfileRowRightElement(profile, isSelected, isFavorite)} | ||
| showDivider={!isLast} | ||
| /> | ||
| ); | ||
| })} | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Item | ||
| title={t('profiles.addProfile')} | ||
| icon={<Ionicons name="add-circle-outline" size={29} color={theme.colors.button.secondary.tint} />} | ||
| onPress={handleAddProfile} | ||
| showChevron={false} | ||
| /> | ||
| </ItemGroup> | ||
| </> | ||
| )} | ||
| </ItemList> | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Page component should be wrapped in memo.
As per coding guidelines, always wrap pages in memo. The ProfilePickerScreen component is not wrapped.
♻️ Wrap in memo
-export default function ProfilePickerScreen() {
+function ProfilePickerScreen() {
// ... component body
}
+
+export default React.memo(ProfilePickerScreen);🤖 Prompt for AI Agents
In `@sources/app/`(app)/new/pick/profile.tsx around lines 19 - 309, The
ProfilePickerScreen page is missing a memo wrapper; update the component export
to use React.memo to prevent unnecessary re-renders by importing memo (or using
React.memo) and exporting the memoized component (e.g., replace the current
default export of ProfilePickerScreen with export default
memo(ProfilePickerScreen) and ensure memo is imported or referenced as
React.memo at the top of the file).
| if (!useProfiles) { | ||
| return ( | ||
| <ItemList style={{ paddingTop: 0 }}> | ||
| <ItemGroup | ||
| title={t('settingsFeatures.profiles')} | ||
| footer={t('settingsFeatures.profilesDisabled')} | ||
| > | ||
| <Item | ||
| title={t('settingsFeatures.profiles')} | ||
| subtitle={t('settingsFeatures.profilesDisabled')} | ||
| icon={<Ionicons name="person-outline" size={29} color="#AF52DE" />} | ||
| rightElement={ | ||
| <Switch | ||
| value={useProfiles} | ||
| onValueChange={setUseProfiles} | ||
| /> | ||
| } | ||
| showChevron={false} | ||
| /> | ||
| </ItemGroup> | ||
| </ItemList> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Critical: Hooks called conditionally after early return.
This hook is being called conditionally, but all hooks must be called in the exact same order in every component render. The early return at lines 45-67 causes useCallback (lines 84, 90) and useMemo (lines 155, 167) to be called conditionally, violating React's Rules of Hooks. When useProfiles toggles, React will throw an error.
Move all hooks above the early return, or restructure to avoid conditional hook calls.
🐛 Suggested fix structure
const ProfileManager = React.memo(function ProfileManager({ onProfileSelect, selectedProfileId }: ProfileManagerProps) {
const { theme } = useUnistyles();
const [useProfiles, setUseProfiles] = useSettingMutable('useProfiles');
const [profiles, setProfiles] = useSettingMutable('profiles');
const [lastUsedProfile, setLastUsedProfile] = useSettingMutable('lastUsedProfile');
const [favoriteProfileIds, setFavoriteProfileIds] = useSettingMutable('favoriteProfiles');
const [editingProfile, setEditingProfile] = React.useState<AIBackendProfile | null>(null);
const [showAddForm, setShowAddForm] = React.useState(false);
const [isEditingDirty, setIsEditingDirty] = React.useState(false);
const isEditingDirtyRef = React.useRef(false);
const experimentsEnabled = useSetting('experiments');
React.useEffect(() => {
isEditingDirtyRef.current = isEditingDirty;
}, [isEditingDirty]);
+ // Move ALL useCallback and useMemo hooks here, BEFORE any early returns
+ const closeEditor = React.useCallback(() => {
+ setShowAddForm(false);
+ setEditingProfile(null);
+ setIsEditingDirty(false);
+ }, []);
+
+ const requestCloseEditor = React.useCallback(() => {
+ // ... implementation
+ }, [closeEditor]);
+
+ const { favoriteProfiles, customProfiles, builtInProfiles, favoriteIds } = React.useMemo(() => {
+ return buildProfileGroups({ customProfiles: profiles, favoriteProfileIds });
+ }, [favoriteProfileIds, profiles]);
+
+ const getProfileBackendSubtitle = React.useCallback((profile) => {
+ // ... implementation
+ }, [experimentsEnabled]);
if (!useProfiles) {
return (
<ItemList style={{ paddingTop: 0 }}>
{/* disabled state UI */}
</ItemList>
);
}
- const closeEditor = React.useCallback(() => { ... }, []);
- const requestCloseEditor = React.useCallback(() => { ... }, [closeEditor]);
- const { ... } = React.useMemo(() => { ... }, [...]);
- const getProfileBackendSubtitle = React.useCallback(...);
// Rest of component...
});🤖 Prompt for AI Agents
In `@sources/app/`(app)/settings/profiles.tsx around lines 45 - 67, The early
return when useProfiles is false causes hooks (the useCallback calls around
lines with setUseProfiles and any useMemo calls later) to be invoked
conditionally; move all hook invocations (the useCallback handlers and useMemo
computations referenced in this file) above the conditional return so that
useCallback, useMemo and any other hooks are always called in the same order,
then render the conditional JSX based on useProfiles (keep the
ItemList/ItemGroup return as-is but after the hooks have been declared).
| Modal.show({ | ||
| component: ItemActionsMenuModal, | ||
| props: { | ||
| title: props.title, | ||
| actions: overflowActions, | ||
| }, | ||
| } as any); | ||
| }, [overflowActions, props.title]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file in question
cat -n sources/components/ItemRowActions.tsx | head -100Repository: leeroybrun/happy
Length of output: 3831
🏁 Script executed:
# Check the Modal type definition
cat -n sources/modal/index.tsRepository: leeroybrun/happy
Length of output: 204
🏁 Script executed:
# Look for ItemActionsMenuModal definition
fd "ItemActionsMenuModal" --type fRepository: leeroybrun/happy
Length of output: 103
🏁 Script executed:
# Search for Modal.show usage patterns in the codebase
rg "Modal\.show" --type ts --type tsx -B 2 -A 2Repository: leeroybrun/happy
Length of output: 87
🏁 Script executed:
# Let's fix the rg command and check Modal.show usage
rg "Modal\.show" -B 2 -A 2Repository: leeroybrun/happy
Length of output: 2277
🏁 Script executed:
# Check ModalManager implementation
cat -n sources/modal/ModalManager.tsRepository: leeroybrun/happy
Length of output: 8318
🏁 Script executed:
# Check the types
cat -n sources/modal/types.tsRepository: leeroybrun/happy
Length of output: 2762
🏁 Script executed:
# Check ItemActionsMenuModal to understand what props it expects
cat -n sources/components/ItemActionsMenuModal.tsx | head -50Repository: leeroybrun/happy
Length of output: 1958
🏁 Script executed:
# Let me check if onClose is somehow being provided automatically or if there's a modal closing mechanism
rg "onClose" sources/modal -A 2 -B 2Repository: leeroybrun/happy
Length of output: 8727
🏁 Script executed:
# Check if ItemActionsMenuModal expects onClose to be optional or if it's required
cat -n sources/components/ItemActionsMenuModal.tsx | head -100Repository: leeroybrun/happy
Length of output: 3967
🏁 Script executed:
# Check if all Modal.show calls use as any cast or just this one
rg "Modal\.show" -A 5 | grep -E "Modal\.show|as any"Repository: leeroybrun/happy
Length of output: 504
🏁 Script executed:
# Check how other modals handle this - do they have the same issue?
cat -n sources/components/CommandPalette/CommandPaletteProvider.tsx | grep -A 10 "Modal.show"Repository: leeroybrun/happy
Length of output: 464
🏁 Script executed:
# Verify how the custom modal component injects onClose
cat -n sources/modal/components/CustomModal.tsxRepository: leeroybrun/happy
Length of output: 1780
🏁 Script executed:
# Let's see the complete CustomModal implementation to understand the injection fully
cat -n sources/modal/components/CustomModal.tsx | head -50Repository: leeroybrun/happy
Length of output: 1780
Remove as any cast and improve Modal type definition.
The as any cast bypasses TypeScript's type checking for Modal.show. While the modal framework automatically injects onClose at runtime (in CustomModal.tsx), TypeScript cannot verify this, and the cast could mask refactoring errors.
Better approaches:
- Update
CustomModalConfiginsources/modal/types.tsto properly express thatonCloseis injected - Create a properly typed wrapper function that encodes the prop injection contract
- Make
onCloseoptional in components that use this pattern
🤖 Prompt for AI Agents
In `@sources/components/ItemRowActions.tsx` around lines 35 - 42, The Modal.show
call in ItemRowActions.tsx currently uses an unsafe "as any" cast; remove that
cast and fix the typing so Modal.show accepts the ItemActionsMenuModal props
without bypassing type checks—either (a) update CustomModalConfig in
sources/modal/types.ts to express that onClose is injected by CustomModal.tsx
(e.g., make onClose optional or modeled as injected), or (b) add a typed
helper/wrapper around Modal.show that accepts the component props (title and
actions) and returns the fully-typed config while guaranteeing onClose will be
provided at runtime; then call Modal.show with the correctly typed config (using
ItemActionsMenuModal, overflowActions and props.title) instead of casting to
any.
| {usePickerSearch && searchVariant === 'header' && ( | ||
| <SearchHeader | ||
| value={searchQuery} | ||
| onChangeText={setSearchQuery} | ||
| placeholder="Search paths..." | ||
| /> | ||
| )} | ||
|
|
||
| <ItemGroup title="Enter Path"> | ||
| <View style={styles.pathInputContainer}> | ||
| <View style={[styles.pathInput, { paddingVertical: 8 }]}> | ||
| <MultiTextInput | ||
| ref={inputRef} | ||
| value={selectedPath} | ||
| onChangeText={onChangeSelectedPath} | ||
| placeholder="Enter path (e.g. /home/user/projects)" | ||
| maxHeight={76} | ||
| paddingTop={8} | ||
| paddingBottom={8} | ||
| /> | ||
| </View> | ||
| </View> | ||
| </ItemGroup> | ||
|
|
||
| {usePickerSearch && searchVariant === 'group' && shouldRenderRecentGroup && ( | ||
| <ItemGroup title="Recent Paths"> | ||
| {effectiveGroupSearchPlacement === 'recent' && ( | ||
| <SearchHeader | ||
| value={searchQuery} | ||
| onChangeText={setSearchQuery} | ||
| placeholder="Search paths..." | ||
| inputRef={searchInputRef} | ||
| onFocus={() => { searchWasFocusedRef.current = true; }} | ||
| onBlur={() => { searchWasFocusedRef.current = false; }} | ||
| containerStyle={{ | ||
| backgroundColor: 'transparent', | ||
| borderBottomWidth: 0, | ||
| }} | ||
| /> | ||
| )} | ||
| {filteredRecentPaths.length === 0 | ||
| ? ( | ||
| <Item | ||
| title={showNoMatchesRow ? 'No matches' : 'No recent paths'} | ||
| showChevron={false} | ||
| showDivider={false} | ||
| disabled={true} | ||
| /> | ||
| ) | ||
| : filteredRecentPaths.map((path, index) => { | ||
| const isSelected = selectedPath.trim() === path; | ||
| const isLast = index === filteredRecentPaths.length - 1; | ||
| const isFavorite = favoritePaths.includes(path); | ||
| return ( | ||
| <Item | ||
| key={path} | ||
| title={path} | ||
| leftElement={<Ionicons name="folder-outline" size={24} color={theme.colors.textSecondary} />} | ||
| onPress={() => setPathAndFocus(path)} | ||
| selected={isSelected} | ||
| showChevron={false} | ||
| rightElement={renderRightElement(path, isSelected, isFavorite)} | ||
| showDivider={!isLast} | ||
| /> | ||
| ); | ||
| })} | ||
| </ItemGroup> | ||
| )} | ||
|
|
||
| {shouldRenderFavoritesGroup && ( | ||
| <ItemGroup title="Favorite Paths"> | ||
| {usePickerSearch && searchVariant === 'group' && effectiveGroupSearchPlacement === 'favorites' && ( | ||
| <SearchHeader | ||
| value={searchQuery} | ||
| onChangeText={setSearchQuery} | ||
| placeholder="Search paths..." | ||
| inputRef={searchInputRef} | ||
| onFocus={() => { searchWasFocusedRef.current = true; }} | ||
| onBlur={() => { searchWasFocusedRef.current = false; }} | ||
| containerStyle={{ | ||
| backgroundColor: 'transparent', | ||
| borderBottomWidth: 0, | ||
| }} | ||
| /> | ||
| )} | ||
| {filteredFavoritePaths.length === 0 | ||
| ? ( | ||
| <Item | ||
| title={showNoMatchesRow ? 'No matches' : 'No favorite paths'} | ||
| showChevron={false} | ||
| showDivider={false} | ||
| disabled={true} | ||
| /> | ||
| ) | ||
| : filteredFavoritePaths.map((path, index) => { | ||
| const isSelected = selectedPath.trim() === path; | ||
| const isLast = index === filteredFavoritePaths.length - 1; | ||
| return ( | ||
| <Item | ||
| key={path} | ||
| title={path} | ||
| leftElement={<Ionicons name="folder-outline" size={18} color={theme.colors.textSecondary} />} | ||
| onPress={() => setPathAndFocus(path)} | ||
| selected={isSelected} | ||
| showChevron={false} | ||
| rightElement={renderRightElement(path, isSelected, true)} | ||
| showDivider={!isLast} | ||
| /> | ||
| ); | ||
| })} | ||
| </ItemGroup> | ||
| )} | ||
|
|
||
| {filteredRecentPaths.length > 0 && searchVariant !== 'group' && ( | ||
| <ItemGroup title="Recent Paths"> | ||
| {filteredRecentPaths.map((path, index) => { | ||
| const isSelected = selectedPath.trim() === path; | ||
| const isLast = index === filteredRecentPaths.length - 1; | ||
| const isFavorite = favoritePaths.includes(path); | ||
| return ( | ||
| <Item | ||
| key={path} | ||
| title={path} | ||
| leftElement={<Ionicons name="folder-outline" size={18} color={theme.colors.textSecondary} />} | ||
| onPress={() => setPathAndFocus(path)} | ||
| selected={isSelected} | ||
| showChevron={false} | ||
| rightElement={renderRightElement(path, isSelected, isFavorite)} | ||
| showDivider={!isLast} | ||
| /> | ||
| ); | ||
| })} | ||
| </ItemGroup> | ||
| )} | ||
|
|
||
| {usePickerSearch && searchVariant === 'group' && shouldRenderSuggestedGroup && ( | ||
| <ItemGroup title="Suggested Paths"> | ||
| {effectiveGroupSearchPlacement === 'suggested' && ( | ||
| <SearchHeader | ||
| value={searchQuery} | ||
| onChangeText={setSearchQuery} | ||
| placeholder="Search paths..." | ||
| inputRef={searchInputRef} | ||
| onFocus={() => { searchWasFocusedRef.current = true; }} | ||
| onBlur={() => { searchWasFocusedRef.current = false; }} | ||
| containerStyle={{ | ||
| backgroundColor: 'transparent', | ||
| borderBottomWidth: 0, | ||
| }} | ||
| /> | ||
| )} | ||
| {filteredSuggestedPaths.length === 0 | ||
| ? ( | ||
| <Item | ||
| title={showNoMatchesRow ? 'No matches' : 'No suggested paths'} | ||
| showChevron={false} | ||
| showDivider={false} | ||
| disabled={true} | ||
| /> | ||
| ) | ||
| : filteredSuggestedPaths.map((path, index) => { | ||
| const isSelected = selectedPath.trim() === path; | ||
| const isLast = index === filteredSuggestedPaths.length - 1; | ||
| const isFavorite = favoritePaths.includes(path); | ||
| return ( | ||
| <Item | ||
| key={path} | ||
| title={path} | ||
| leftElement={<Ionicons name="folder-outline" size={24} color={theme.colors.textSecondary} />} | ||
| onPress={() => setPathAndFocus(path)} | ||
| selected={isSelected} | ||
| showChevron={false} | ||
| rightElement={renderRightElement(path, isSelected, isFavorite)} | ||
| showDivider={!isLast} | ||
| /> | ||
| ); | ||
| })} | ||
| </ItemGroup> | ||
| )} | ||
|
|
||
| {filteredRecentPaths.length === 0 && filteredSuggestedPaths.length > 0 && searchVariant !== 'group' && ( | ||
| <ItemGroup title="Suggested Paths"> | ||
| {filteredSuggestedPaths.map((path, index) => { | ||
| const isSelected = selectedPath.trim() === path; | ||
| const isLast = index === filteredSuggestedPaths.length - 1; | ||
| const isFavorite = favoritePaths.includes(path); | ||
| return ( | ||
| <Item | ||
| key={path} | ||
| title={path} | ||
| leftElement={<Ionicons name="folder-outline" size={24} color={theme.colors.textSecondary} />} | ||
| onPress={() => setPathAndFocus(path)} | ||
| selected={isSelected} | ||
| showChevron={false} | ||
| rightElement={renderRightElement(path, isSelected, isFavorite)} | ||
| showDivider={!isLast} | ||
| /> | ||
| ); | ||
| })} | ||
| </ItemGroup> | ||
| )} | ||
|
|
||
| {usePickerSearch && searchVariant === 'group' && shouldRenderFallbackGroup && ( | ||
| <ItemGroup title="Paths"> | ||
| <SearchHeader | ||
| value={searchQuery} | ||
| onChangeText={setSearchQuery} | ||
| placeholder="Search paths..." | ||
| inputRef={searchInputRef} | ||
| onFocus={() => { searchWasFocusedRef.current = true; }} | ||
| onBlur={() => { searchWasFocusedRef.current = false; }} | ||
| containerStyle={{ | ||
| backgroundColor: 'transparent', | ||
| borderBottomWidth: 0, | ||
| }} | ||
| /> | ||
| <Item | ||
| title={showNoMatchesRow ? 'No matches' : 'No paths'} |
There was a problem hiding this comment.
Replace hardcoded picker strings with translations.
Labels and placeholders are currently hardcoded. Please use t(...) keys and add the new strings to translation files.
🌐 Example fix pattern
- placeholder="Search paths..."
+ placeholder={t('pathPicker.searchPlaceholder')}
@@
- <ItemGroup title="Enter Path">
+ <ItemGroup title={t('pathPicker.enterPathTitle')}>
@@
- placeholder="Enter path (e.g. /home/user/projects)"
+ placeholder={t('pathPicker.enterPathPlaceholder')}As per coding guidelines, all user-visible strings must use t(...) and be present in all locale files.
🤖 Prompt for AI Agents
In `@sources/components/newSession/PathSelector.tsx` around lines 230 - 447,
Replace all hardcoded user-facing strings in PathSelector's JSX with translation
keys using t(...): update title props on ItemGroup ("Enter Path", "Recent
Paths", "Favorite Paths", "Suggested Paths", "Paths"), placeholders ("Search
paths...", "Enter path (e.g. /home/user/projects)") passed to SearchHeader and
MultiTextInput, and Item titles/disabled messages ("No matches", "No recent
paths", "No favorite paths", "No suggested paths", "No paths") to use
t('pathSelector.xxx') (or your chosen key namespace); modify usages in
components SearchHeader, MultiTextInput, ItemGroup and Item (and any
rightElement text) to call t(...) instead of literals, then add those keys to
all locale translation files so each locale includes the new entries.
| return ( | ||
| <View | ||
| style={[ | ||
| { | ||
| width: size, | ||
| height: size, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| style, | ||
| ]} | ||
| > | ||
| {glyphs.length === 1 ? ( | ||
| <Text style={{ fontSize: Math.round(size * glyphs[0].factor), color: theme.colors.textSecondary, ...Typography.default() }}> | ||
| {glyphs[0].glyph} | ||
| </Text> | ||
| ) : ( | ||
| <View style={{ flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 0 }}> | ||
| {glyphs.map((item) => { | ||
| const fontSize = Math.round(size * multiScale * item.factor); | ||
| return ( | ||
| <Text | ||
| key={item.key} | ||
| style={{ | ||
| fontSize, | ||
| lineHeight: Math.max(10, Math.round(fontSize * 0.92)), | ||
| color: theme.colors.textSecondary, | ||
| ...Typography.default(), | ||
| }} | ||
| > |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
cd /root/repo && find . -name "ProfileCompatibilityIcon.tsx" -type fRepository: leeroybrun/happy
Length of output: 120
🏁 Script executed:
cd /root/repo && cat -n sources/components/newSession/ProfileCompatibilityIcon.tsxRepository: leeroybrun/happy
Length of output: 120
🏁 Script executed:
find . -name "ProfileCompatibilityIcon.tsx" -type fRepository: leeroybrun/happy
Length of output: 120
🏁 Script executed:
cat -n ./sources/components/newSession/ProfileCompatibilityIcon.tsxRepository: leeroybrun/happy
Length of output: 3403
Move styles to StyleSheet.create (function mode).
Inline styles with theme access should be centralized in unistyles StyleSheet.create using function mode for consistency and caching. Runtime-computed properties (fontSize based on size, lineHeight calculations) remain as inline styles.
♻️ Proposed refactor
-import { useUnistyles } from 'react-native-unistyles';
+import { StyleSheet, useUnistyles } from 'react-native-unistyles';
import { Typography } from '@/constants/Typography';
import type { AIBackendProfile } from '@/sync/settings';
import { useSetting } from '@/sync/storage';
+const styles = StyleSheet.create((theme) => ({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ stack: {
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 0,
+ },
+ glyph: {
+ color: theme.colors.textSecondary,
+ ...Typography.default(),
+ },
+}));
+
export function ProfileCompatibilityIcon({ profile, size = 32, style }: Props) {
const { theme } = useUnistyles();
const experimentsEnabled = useSetting('experiments');
@@
return (
- <View
- style={[
- {
- width: size,
- height: size,
- alignItems: 'center',
- justifyContent: 'center',
- },
- style,
- ]}
- >
+ <View style={[styles.container, { width: size, height: size }, style]}>
{glyphs.length === 1 ? (
- <Text style={{ fontSize: Math.round(size * glyphs[0].factor), color: theme.colors.textSecondary, ...Typography.default() }}>
+ <Text style={[styles.glyph, { fontSize: Math.round(size * glyphs[0].factor) }]}>
{glyphs[0].glyph}
</Text>
) : (
- <View style={{ flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 0 }}>
+ <View style={styles.stack}>
{glyphs.map((item) => {
const fontSize = Math.round(size * multiScale * item.factor);
return (
<Text
key={item.key}
- style={{
- fontSize,
- lineHeight: Math.max(10, Math.round(fontSize * 0.92)),
- color: theme.colors.textSecondary,
- ...Typography.default(),
- }}
+ style={[
+ styles.glyph,
+ {
+ fontSize,
+ lineHeight: Math.max(10, Math.round(fontSize * 0.92)),
+ },
+ ]}
>
{item.glyph}
</Text>
);
})}
</View>
)}
</View>
);
}📝 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.
| return ( | |
| <View | |
| style={[ | |
| { | |
| width: size, | |
| height: size, | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| }, | |
| style, | |
| ]} | |
| > | |
| {glyphs.length === 1 ? ( | |
| <Text style={{ fontSize: Math.round(size * glyphs[0].factor), color: theme.colors.textSecondary, ...Typography.default() }}> | |
| {glyphs[0].glyph} | |
| </Text> | |
| ) : ( | |
| <View style={{ flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 0 }}> | |
| {glyphs.map((item) => { | |
| const fontSize = Math.round(size * multiScale * item.factor); | |
| return ( | |
| <Text | |
| key={item.key} | |
| style={{ | |
| fontSize, | |
| lineHeight: Math.max(10, Math.round(fontSize * 0.92)), | |
| color: theme.colors.textSecondary, | |
| ...Typography.default(), | |
| }} | |
| > | |
| import { StyleSheet, useUnistyles } from 'react-native-unistyles'; | |
| import { Typography } from '@/constants/Typography'; | |
| import type { AIBackendProfile } from '@/sync/settings'; | |
| import { useSetting } from '@/sync/storage'; | |
| const styles = StyleSheet.create((theme) => ({ | |
| container: { | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| }, | |
| stack: { | |
| flexDirection: 'column', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| gap: 0, | |
| }, | |
| glyph: { | |
| color: theme.colors.textSecondary, | |
| ...Typography.default(), | |
| }, | |
| })); | |
| export function ProfileCompatibilityIcon({ profile, size = 32, style }: Props) { | |
| const { theme } = useUnistyles(); | |
| const experimentsEnabled = useSetting('experiments'); | |
| return ( | |
| <View style={[styles.container, { width: size, height: size }, style]}> | |
| {glyphs.length === 1 ? ( | |
| <Text style={[styles.glyph, { fontSize: Math.round(size * glyphs[0].factor) }]}> | |
| {glyphs[0].glyph} | |
| </Text> | |
| ) : ( | |
| <View style={styles.stack}> | |
| {glyphs.map((item) => { | |
| const fontSize = Math.round(size * multiScale * item.factor); | |
| return ( | |
| <Text | |
| key={item.key} | |
| style={[ | |
| styles.glyph, | |
| { | |
| fontSize, | |
| lineHeight: Math.max(10, Math.round(fontSize * 0.92)), | |
| }, | |
| ]} | |
| > | |
| {item.glyph} | |
| </Text> | |
| ); | |
| })} | |
| </View> | |
| )} | |
| </View> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In `@sources/components/newSession/ProfileCompatibilityIcon.tsx` around lines 33 -
62, ProfileCompatibilityIcon uses inline style objects that should be moved into
unistyles StyleSheet.create in function mode to enable theme-based caching;
create a styles factory via StyleSheet.create(() => ({ container,
singleGlyphText, multiContainer, mappedTextBase })) and replace the static parts
of the inline styles for the outer View (container), the single-glyph Text
(singleGlyphText), the inner column View (multiContainer) and the per-item Text
base (mappedTextBase) with those style refs, while preserving runtime-computed
properties (fontSize, lineHeight) inline in the JSX; keep references to
theme.colors.textSecondary, Typography.default(), and the dynamic values size,
multiScale and item.factor when applying styles so only static/theme-dependent
parts move into the StyleSheet function.
Stop logging elevenlabs conversation tokens to the console. Fixes slopus/happy-server#10
…gging Fix #10: stop logging conversation tokens
Context
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 and reviewer-friendly (no blame): this is mostly stabilization work after a very large merge.
Summary (what this PR does)
What #272 introduced (and what we’re fixing here)
New Session (standard flow) regressions
Wizard + pickers inconsistent with existing UI
Profiles “leaked” into default behavior
Non-UI safety/reliability issues (PR272 logic surface)
profileSync.ts) that wasn’t actually wired.Profiles/editor correctness
id: ''.5571035).Navigation coherence (web + native + unmount safety)
04628ed,5571035).Global daemon env fetch (and secret exposure risk)
envVarRefs/daemonEnvfetch in/newthat queried the daemon for all${VAR}references across all profiles (no secret filtering).04628ed) and keeping env resolution scoped with secret-like filtering in preview paths (1a36c61).Model meta resets (pre-existing behavior, adjusted here)
meta.model = null/meta.fallbackModel = nullon every message (see pre-mergefcc97a7:sources/sync/sync.ts).modelkey (evennull) as a reset-to-default signal, this PR now omits these keys unless explicitly set (8adde3f)Notable commits (suggested review order)
High-signal / “why the diff is large” commits:
04628edrefactor(profiles): remove provider config objects5571035fix(new-session): switch profile picker to id-based navigation1a36c61fix(env): align template semantics and preview safety8adde3ffix(sync): stop resetting model meta and gate logs21c7968refactor(i18n): move translation types to _types4b70ee6fix(env-vars): batch resolve in editor and support :=Earlier stabilization commits from this branch (still part of the story):
fd65d9c,f0d2a23d0f96fd,1876c44,6c89ad73d8361d,0ba2ea4,7e2d9b3,4324a97fc7b7bbTests / verification
Automated
yarn typecheckyarn testNew/updated tests were added around:
sources/app/(app)/new/pick/profilePickerRouting.test.ts)sources/components/EnvironmentVariablesList.keys.test.ts,sources/hooks/useEnvironmentVariables.test.ts)sources/sync/messageMeta.test.ts)Manual (high-value checks)
/newpreserves selection.model:nullmeta on every send.Summary by CodeRabbit
Release Notes
New Features
Improvements
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.