Feature/add auto mode settings and fix bug#368
Conversation
将 readonly unknown[] 改为 readonly LangfuseInputMessage[], 其中 LangfuseInputMessage = UserMessage | AssistantMessage | ChatCompletionMessageParam, 让调用方获得编译期类型检查。
将左右键枚举值切换从依赖 DOM 焦点的 onKeyDown 改为 useKeybindings 系统, 确保按键在任何焦点状态下都能正确响应。同时修复 isSearchMode 初始值和布局问题。 Co-Authored-By: Claude Opus 4.7 <[email protected]>
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- 在 REPL 底栏添加 RSS 内存使用显示,512MB 以下 dimColor,512MB-1GB warning 色,1GB 以上 error 色 - auto 权限模式不再依赖 TRANSCRIPT_CLASSIFIER feature flag,classifier 不可用时 fallback 到 prompting - Config 面板 defaultPermissionMode 使用类型安全的 permissionModeFromString,显示改用 shortTitle - bypassPermissions title 缩短为 Bypass 与 shortTitle 一致 Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughUnconditionally enable the Changes
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Co-Authored-By: Claude Opus 4.7 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/utils/settings/types.ts (1)
1022-1025:⚠️ Potential issue | 🟠 MajorReconcile
disableAutoMode— defined in two schema locations with inconsistent gating.A new top-level
disableAutoMode(lines 1022–1025, unconditional) has been added while the originalpermissions.disableAutoMode(lines 67–74, gated onTRANSCRIPT_CLASSIFIER) remains. This creates:
- Inconsistent validation gating: The permissions field is conditionally included in the schema (only when the feature flag is enabled), while the top-level field is always present. The export validation (settings.ts line 577) only manages the permissions version conditionally.
- Unmanaged field: The top-level
disableAutoModeis not listed in any validation set, unlike other fields in its schema section.- Consumer workaround: Code in
permissionSetup.tschecks both locations with OR logic (settings.disableAutoMode || settings.permissions.disableAutoMode), indicating this dual-location pattern is unintended rather than deliberate.This breaks the established pattern where feature-flagged fields use conditional spreads (e.g.,
...(feature('FLAG') ? { field: ... } : {})). Either remove the top-level field and keep the permissions-gated version, or gate the top-level field consistently and deprecate the permissions version with a migration note.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/settings/types.ts` around lines 1022 - 1025, The repo now defines disableAutoMode in two places (top-level disableAutoMode and permissions.disableAutoMode) causing inconsistent validation and consumer OR checks; fix by reconciling to a single gated location: either remove the unconditional top-level disableAutoMode or gate it behind the same TRANSCRIPT_CLASSIFIER feature flag (using the same conditional spread pattern) and update the export validation in settings.ts (the code around the export validation mentioned) to validate the chosen location only; also update permissionSetup.ts to read from the single agreed field and, if removing the old permissions.disableAutoMode, add a short migration/deprecation note where permissions.disableAutoMode was defined.
🧹 Nitpick comments (4)
src/services/langfuse/convert.ts (1)
14-14: Use the publicopenai/resources/chatentry point instead of the internal.mjssubpath.Importing from
openai/resources/chat/completions/completions.mjsbypasses the package's documented public API and risks breaking on minor version upgrades. The OpenAI SDK v6 officially exports all chat-related types (includingChatCompletionMessageParam) from the stableopenai/resources/chatpath. This same import pattern appears in multiple files throughout the codebase (src/services/api/openai/,packages/@ant/model-provider/) and should be updated consistently.♻️ Suggested change
-import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions/completions.mjs' +import type { ChatCompletionMessageParam } from 'openai/resources/chat'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/langfuse/convert.ts` at line 14, Replace the internal subpath import for ChatCompletionMessageParam with the package's public chat entry: update the import that currently references openai/resources/chat/completions/completions.mjs to import ChatCompletionMessageParam from openai/resources/chat; adjust the import statement in src/services/langfuse/convert.ts (the line importing ChatCompletionMessageParam) so it uses the public entrypoint to avoid reliance on internal .mjs paths.src/components/PromptInput/PromptInputFooterLeftSide.tsx (1)
70-84: Redundant unit round-trip; pass bytes straight toformatFileSize.
update()convertsrss → mbonly to multiply back by1024 * 1024when handing off toformatFileSize(which already accepts bytes persrc/utils/format.ts). Compare thresholds in bytes and drop the intermediate.♻️ Proposed simplification
- function update(): void { - const mb = process.memoryUsage().rss / (1024 * 1024) - const level = mb >= 1024 ? 'error' : mb >= 512 ? 'warning' : 'normal' - const text = formatFileSize(mb * 1024 * 1024) - setState(prev => (prev?.text === text ? prev : { text, level })) - } + const WARNING_BYTES = 512 * 1024 * 1024 + const ERROR_BYTES = 1024 * 1024 * 1024 + function update(): void { + const rss = process.memoryUsage().rss + const level: RssState['level'] = + rss >= ERROR_BYTES ? 'error' : rss >= WARNING_BYTES ? 'warning' : 'normal' + const text = formatFileSize(rss) + setState(prev => + prev?.text === text && prev.level === level ? prev : { text, level }, + ) + }Also note: the existing dedup keys only on
text, so a level transition that happens to land on the same formatted text won't propagate — the diff above keys on both.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/PromptInput/PromptInputFooterLeftSide.tsx` around lines 70 - 84, useRssDisplay: the update() function unnecessarily converts rss to megabytes then multiplies back when calling formatFileSize and only dedups on text (so level changes may be dropped); fix by keeping rssBytes = process.memoryUsage().rss and use that directly with formatFileSize(rssBytes), compute level thresholds in bytes (e.g. 1024*1024*512 and 1024*1024*1024) and update setState to compare both text and level (prev?.text === text && prev?.level === level) before skipping state updates; keep RSS_UPDATE_INTERVAL_MS and function names (useRssDisplay, update, RssState, formatFileSize) the same.src/keybindings/schema.ts (1)
171-172: Group new actions with existingselect:*block.
select:previousValueandselect:nextValuewere appended after thesettings:*group rather than next to lines 158–161 where the otherselect:*actions live. Purely cosmetic, but it makes future grep/diffs easier.♻️ Suggested move
// Select component actions (distinct from confirm: to avoid collisions) 'select:next', 'select:previous', 'select:accept', 'select:cancel', + 'select:previousValue', + 'select:nextValue', // Plugin dialog actions ... // Settings config panel actions 'settings:search', 'settings:retry', 'settings:close', - 'select:previousValue', - 'select:nextValue', // Voice actions🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/keybindings/schema.ts` around lines 171 - 172, The two keybinding entries 'select:previousValue' and 'select:nextValue' were appended after the `settings:*` group instead of being grouped with the other `select:*` actions; move the 'select:previousValue' and 'select:nextValue' entries so they sit adjacent to the existing `select:*` entries (the same block that contains the other 'select:' strings) in src/keybindings/schema.ts, keeping the same array/list formatting and ordering convention as the other `select:` actions to preserve grouping for easier greps and diffs.src/utils/permissions/PermissionMode.ts (1)
80-86: Drop redundant type assertions on theautoconfig.
'warning'is already a validModeColorKeyliteral and'default'is already a validExternalPermissionModeliteral — the surroundingPERMISSION_MODE_CONFIGdeclaration provides the contextual type, so no cast is needed. The other entries in this object don't carry these casts.♻️ Proposed cleanup
auto: { title: 'Auto', shortTitle: 'Auto', symbol: '⏵⏵', - color: 'warning' as ModeColorKey, - external: 'default' as ExternalPermissionMode, + color: 'warning', + external: 'default', },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/permissions/PermissionMode.ts` around lines 80 - 86, Remove the redundant type assertions in the `auto` entry of the `PERMISSION_MODE_CONFIG` object in PermissionMode.ts: drop `as ModeColorKey` from the `color` property and `as ExternalPermissionMode` from the `external` property so the literals `'warning'` and `'default'` rely on the surrounding contextual type; update the `auto` config to match the other entries that do not use casts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/PromptInput/PromptInputFooterLeftSide.tsx`:
- Around line 452-463: The always-on RSS entry (rssState from useRssDisplay)
causes parts to be non-empty and breaks the empty-state fallbacks; fix by
ensuring the RSS element does not count toward the "real" emptiness checks:
either stop pushing the RSS element into parts used by the emptiness logic (gate
it behind a separate rssPart variable or push it into partsAfterEmptyChecks) or
push the RSS element last and change all empty checks to compute emptiness from
a filteredParts = parts.filter(p => p !== rssPart) (used by the if (parts.length
=== 0 && !tasksPart && !modePart && showHint) and if (parts.length === 0 &&
!tasksPart && !modePart) conditions); update references to rssState, parts,
tasksPart, modePart, and showHint accordingly so the RSS indicator is rendered
but does not disable the empty-state branches.
- Around line 452-463: Add a feature-flag for the RSS footer and gate rendering
on it: add rssFooterEnabled?: boolean to the GlobalConfig type, ensure
getGlobalConfig() exposes that flag (similar to prStatusFooterEnabled), and
update PromptInputFooterLeftSide to only render the rssState block when rssState
is truthy AND getGlobalConfig().rssFooterEnabled is true; reference the rssState
rendering block in PromptInputFooterLeftSide and the GlobalConfig type so the
footer respects the new setting.
In `@src/components/Settings/Config.tsx`:
- Around line 1551-1552: The current keybinding handlers call toggleSetting()
for both 'select:previousValue' and 'select:nextValue', so left and right both
advance forward; update toggleSetting (used in the select handlers and
referenced in handleKeyDown) to accept a direction parameter (e.g., direction: 1
| -1) or add two helpers like toggleSettingNext and toggleSettingPrev, then wire
'select:nextValue' to advance and 'select:previousValue' to decrement (use
modulo arithmetic to wrap negative indices) so the previous/next keybindings
produce opposite traversal through setting.options; ensure any branches (e.g.,
autoUpdatesChannel) that don't need direction keep current behavior.
In `@src/utils/permissions/PermissionMode.ts`:
- Line 67: Update the failing test to match the current PermissionMode enum
label: change the expected title in the test in PermissionMode.test.ts from
"Bypass Permissions" to "Bypass" so it aligns with the implementation (see
PermissionMode's title property and the test assertion referencing it); after
updating the expectation, run tests and quickly scan usages that depend on the
label (PermissionDecisionDebugInfo, permissions approval message,
PromptInputFooterLeftSide) to ensure the shorter "Bypass" still reads clearly in
those UI/debug messages.
---
Outside diff comments:
In `@src/utils/settings/types.ts`:
- Around line 1022-1025: The repo now defines disableAutoMode in two places
(top-level disableAutoMode and permissions.disableAutoMode) causing inconsistent
validation and consumer OR checks; fix by reconciling to a single gated
location: either remove the unconditional top-level disableAutoMode or gate it
behind the same TRANSCRIPT_CLASSIFIER feature flag (using the same conditional
spread pattern) and update the export validation in settings.ts (the code around
the export validation mentioned) to validate the chosen location only; also
update permissionSetup.ts to read from the single agreed field and, if removing
the old permissions.disableAutoMode, add a short migration/deprecation note
where permissions.disableAutoMode was defined.
---
Nitpick comments:
In `@src/components/PromptInput/PromptInputFooterLeftSide.tsx`:
- Around line 70-84: useRssDisplay: the update() function unnecessarily converts
rss to megabytes then multiplies back when calling formatFileSize and only
dedups on text (so level changes may be dropped); fix by keeping rssBytes =
process.memoryUsage().rss and use that directly with formatFileSize(rssBytes),
compute level thresholds in bytes (e.g. 1024*1024*512 and 1024*1024*1024) and
update setState to compare both text and level (prev?.text === text &&
prev?.level === level) before skipping state updates; keep
RSS_UPDATE_INTERVAL_MS and function names (useRssDisplay, update, RssState,
formatFileSize) the same.
In `@src/keybindings/schema.ts`:
- Around line 171-172: The two keybinding entries 'select:previousValue' and
'select:nextValue' were appended after the `settings:*` group instead of being
grouped with the other `select:*` actions; move the 'select:previousValue' and
'select:nextValue' entries so they sit adjacent to the existing `select:*`
entries (the same block that contains the other 'select:' strings) in
src/keybindings/schema.ts, keeping the same array/list formatting and ordering
convention as the other `select:` actions to preserve grouping for easier greps
and diffs.
In `@src/services/langfuse/convert.ts`:
- Line 14: Replace the internal subpath import for ChatCompletionMessageParam
with the package's public chat entry: update the import that currently
references openai/resources/chat/completions/completions.mjs to import
ChatCompletionMessageParam from openai/resources/chat; adjust the import
statement in src/services/langfuse/convert.ts (the line importing
ChatCompletionMessageParam) so it uses the public entrypoint to avoid reliance
on internal .mjs paths.
In `@src/utils/permissions/PermissionMode.ts`:
- Around line 80-86: Remove the redundant type assertions in the `auto` entry of
the `PERMISSION_MODE_CONFIG` object in PermissionMode.ts: drop `as ModeColorKey`
from the `color` property and `as ExternalPermissionMode` from the `external`
property so the literals `'warning'` and `'default'` rely on the surrounding
contextual type; update the `auto` config to match the other entries that do not
use casts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bb5f71d5-55ca-4f65-bc96-d7870e608187
📒 Files selected for processing (10)
packages/builtin-tools/src/tools/PowerShellTool/PowerShellTool.tsxsrc/components/PromptInput/PromptInputFooterLeftSide.tsxsrc/components/Settings/Config.tsxsrc/keybindings/defaultBindings.tssrc/keybindings/schema.tssrc/services/langfuse/__tests__/langfuse.test.tssrc/services/langfuse/convert.tssrc/types/permissions.tssrc/utils/permissions/PermissionMode.tssrc/utils/settings/types.ts
| // RSS memory indicator — always visible | ||
| ...(rssState | ||
| ? [ | ||
| <Text | ||
| key="rss" | ||
| dimColor={rssState.level === 'normal'} | ||
| color={rssState.level === 'error' ? 'error' : rssState.level === 'warning' ? 'warning' : undefined} | ||
| > | ||
| {rssState.text} | ||
| </Text>, | ||
| ] | ||
| : []), |
There was a problem hiding this comment.
Always-on RSS entry breaks the empty-state fallbacks.
useRssDisplay() calls update() synchronously in its useEffect, so rssState becomes non-null on the second render and stays non-null for the rest of the session. That means parts will essentially always contain ≥1 element, which silently disables two existing branches:
- Line 554:
if (parts.length === 0 && !tasksPart && !modePart && showHint)— the "? for shortcuts" hint will no longer be shown to new users on an otherwise empty footer. - Line 640:
if (parts.length === 0 && !tasksPart && !modePart)— the fullscreen row-reservation fallback (the deliberately-rendered<Text> </Text>) is now dead code; the component will instead render the truncating Box. The comment block above line 632 explicitly calls out that this empty-parts path matters for stable footer height.
Either gate the RSS push so it doesn't count for these checks, or push it last and re-evaluate emptiness against a "real content" subset.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/PromptInput/PromptInputFooterLeftSide.tsx` around lines 452 -
463, The always-on RSS entry (rssState from useRssDisplay) causes parts to be
non-empty and breaks the empty-state fallbacks; fix by ensuring the RSS element
does not count toward the "real" emptiness checks: either stop pushing the RSS
element into parts used by the emptiness logic (gate it behind a separate
rssPart variable or push it into partsAfterEmptyChecks) or push the RSS element
last and change all empty checks to compute emptiness from a filteredParts =
parts.filter(p => p !== rssPart) (used by the if (parts.length === 0 &&
!tasksPart && !modePart && showHint) and if (parts.length === 0 && !tasksPart &&
!modePart) conditions); update references to rssState, parts, tasksPart,
modePart, and showHint accordingly so the RSS indicator is rendered but does not
disable the empty-state branches.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
# First, let's understand the context of the file and the rssState
head -n 470 src/components/PromptInput/PromptInputFooterLeftSide.tsx | tail -n 30Repository: claude-code-best/claude-code
Length of output: 946
🏁 Script executed:
# Check for rssState definition and initialization
rg "rssState" src/components/PromptInput/PromptInputFooterLeftSide.tsx -B 3 -A 1Repository: claude-code-best/claude-code
Length of output: 585
🏁 Script executed:
# Search for isPrStatusEnabled pattern mentioned in the review
rg "isPrStatusEnabled" --type ts --type tsxRepository: claude-code-best/claude-code
Length of output: 100
🏁 Script executed:
# Search for getGlobalConfig usage pattern
rg "getGlobalConfig" --type ts --type tsx | head -20Repository: claude-code-best/claude-code
Length of output: 100
🏁 Script executed:
# Search for feature flag patterns in the codebase
rg "feature\(" --type ts --type tsx | head -20Repository: claude-code-best/claude-code
Length of output: 100
🏁 Script executed:
# Check if there are other RSS-related configurations
rg "rss" src/ --type ts --type tsx -i | head -30Repository: claude-code-best/claude-code
Length of output: 100
🏁 Script executed:
# Search for useRssDisplay hook definition
rg "useRssDisplay" -A 10Repository: claude-code-best/claude-code
Length of output: 2259
🏁 Script executed:
# Search for isPrStatusEnabled without type filter
rg "isPrStatusEnabled" -B 2 -A 5Repository: claude-code-best/claude-code
Length of output: 1975
🏁 Script executed:
# Search for feature flag usage patterns
rg "feature\(" -B 1 -A 1 | head -40Repository: claude-code-best/claude-code
Length of output: 2409
🏁 Script executed:
# Check the imports and context of the PromptInputFooterLeftSide file
head -n 50 src/components/PromptInput/PromptInputFooterLeftSide.tsxRepository: claude-code-best/claude-code
Length of output: 2573
🏁 Script executed:
# Look for config/shouldShow patterns related to RSS
rg "shouldShow|config\|getGlobalConfig" src/ -A 2 -B 2 | head -50Repository: claude-code-best/claude-code
Length of output: 2451
🏁 Script executed:
# Find getGlobalConfig definition
rg "getGlobalConfig" -B 2 -A 10 | head -60Repository: claude-code-best/claude-code
Length of output: 4131
🏁 Script executed:
# Check if rssFooterEnabled exists in config
rg "rssFooterEnabled"Repository: claude-code-best/claude-code
Length of output: 54
🏁 Script executed:
# Find the config file structure
fd "config" -type f | grep -E "\.(ts|js)$" | head -10Repository: claude-code-best/claude-code
Length of output: 243
🏁 Script executed:
# Search for GlobalConfig type definition
rg "interface GlobalConfig|type GlobalConfig" -A 15Repository: claude-code-best/claude-code
Length of output: 5876
🏁 Script executed:
# Check RSS_UPDATE_INTERVAL_MS definition
rg "RSS_UPDATE_INTERVAL_MS"Repository: claude-code-best/claude-code
Length of output: 285
🏁 Script executed:
# Get the full GlobalConfig type definition
sed -n '/^export type GlobalConfig = {/,/^}/p' src/utils/config.tsRepository: claude-code-best/claude-code
Length of output: 18408
🏁 Script executed:
# Also check GLOBAL_CONFIG_KEYS to see all available keys
rg "GLOBAL_CONFIG_KEYS" -A 30 src/utils/config.tsRepository: claude-code-best/claude-code
Length of output: 2073
🏁 Script executed:
# Check if there's any mention of RSS or memory monitoring in config
rg "memory|rss|footer" -i src/utils/config.tsRepository: claude-code-best/claude-code
Length of output: 1015
Gate RSS footer behind config option, similar to prStatusFooterEnabled.
The RSS memory indicator currently displays for all users every 5 seconds with no way to disable it. This differs from the PR status footer, which respects getGlobalConfig().prStatusFooterEnabled. Memory bytes ticking in the footer may confuse non-developers and appears to be intended as a debugging aid rather than a default-on feature.
Apply the same gating pattern: add rssFooterEnabled?: boolean to GlobalConfig and check it in the render condition alongside the existence of rssState.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/PromptInput/PromptInputFooterLeftSide.tsx` around lines 452 -
463, Add a feature-flag for the RSS footer and gate rendering on it: add
rssFooterEnabled?: boolean to the GlobalConfig type, ensure getGlobalConfig()
exposes that flag (similar to prStatusFooterEnabled), and update
PromptInputFooterLeftSide to only render the rssState block when rssState is
truthy AND getGlobalConfig().rssFooterEnabled is true; reference the rssState
rendering block in PromptInputFooterLeftSide and the GlobalConfig type so the
footer respects the new setting.
| 'select:previousValue': () => toggleSetting(), | ||
| 'select:nextValue': () => toggleSetting(), |
There was a problem hiding this comment.
select:previousValue cycles forward — same as select:nextValue.
toggleSetting() only ever advances by +1 (line 1510: (currentIndex + 1) % setting.options.length). Both new keybindings invoke it, so left arrow behaves identically to right arrow. Users will expect left to step backward through enum values (e.g., default → plan → auto → default going right; reverse going left).
This preserves the prior handleKeyDown behavior at line 1593, but the names select:previousValue / select:nextValue now actively suggest a directionality that doesn't exist.
🐛 Proposed fix
Add a direction arg to toggleSetting, or split into two helpers:
- const toggleSetting = useCallback(() => {
+ const toggleSetting = useCallback((direction: 1 | -1 = 1) => {
const setting = filteredSettingsItems[selectedIndex];
...
if (setting.type === 'enum') {
isDirty.current = true;
const currentIndex = setting.options.indexOf(setting.value);
- const nextIndex = (currentIndex + 1) % setting.options.length;
+ const len = setting.options.length;
+ const nextIndex = (currentIndex + direction + len) % len;
setting.onChange(setting.options[nextIndex]!);
return;
}
}, [...]);- 'select:previousValue': () => toggleSetting(),
- 'select:nextValue': () => toggleSetting(),
+ 'select:previousValue': () => toggleSetting(-1),
+ 'select:nextValue': () => toggleSetting(1),(autoUpdatesChannel and other branches that don't read direction will keep the existing forward-only semantics.)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/Settings/Config.tsx` around lines 1551 - 1552, The current
keybinding handlers call toggleSetting() for both 'select:previousValue' and
'select:nextValue', so left and right both advance forward; update toggleSetting
(used in the select handlers and referenced in handleKeyDown) to accept a
direction parameter (e.g., direction: 1 | -1) or add two helpers like
toggleSettingNext and toggleSettingPrev, then wire 'select:nextValue' to advance
and 'select:previousValue' to decrement (use modulo arithmetic to wrap negative
indices) so the previous/next keybindings produce opposite traversal through
setting.options; ensure any branches (e.g., autoUpdatesChannel) that don't need
direction keep current behavior.
Summary by CodeRabbit
New Features
Bug Fixes
Improvements
Tests