Feature: Reorganized settings menu#563
Conversation
- Replace flat settings list with 8-category nested navigation - Add breadcrumb titles (Settings · Appearance · Theme) - Add Shift+Tab back-navigation at all levels - Add Keep/Discard prompt for unsaved changes - Migrate existing 5 settings into Appearance, Input, Behavior categories - Add new Behavior panels: Auto-Compact, Sessions, Default Mode, Reasoning Traces - Add Environment category with read-only NANOCODER_* env var display - Add config-writer.ts utility for writing to agents.config.json - Add settings-menu-types.ts with category definitions and path utilities - Add placeholder panels for unimplemented settings (Providers, MCPs, Web Search, Advanced) - Update settings command description - Update tests for new category structure
- Add Tool Auto-Approval panel (Providers category) — read-only display of alwaysAllow tool lists from agents.config.json - Add Web Search panel — API key input with masking for Brave Search - Wire up new panels in settings panel router - Placeholder panels remain for wizard entry points (configure providers, MCP servers, IDE, tune, config files) to be connected in future phases
- Update descriptions for /setup-config, /setup-providers, /setup-mcp, /ide, /tune, /copilot-login, /codex-login with deprecation banners pointing to /settings - Add runtime logWarning for /setup-config - Update lazy-registry inline descriptions to match - Update /settings description to reflect expanded scope
Maps all 20+ documented configuration parameters to their TUI settings category, with clear rationale for excluded parameters (env vars, per-provider granularity, logging). Also documents future planned parameters.
The ideCommand description was updated to include a deprecation notice. The test was asserting an exact string match; updated to use substring checks that verify both the original text and the deprecation tag.
- Fix stuck Keep/Discard prompt: replaced useInput handler with SelectInput onSelect callback (Enter was being swallowed by SelectInput's internal handler) - Add changesSummary to dirty state: shows category and panel name in the prompt (e.g. "Appearance → Theme changed") - Add summary field to DirtyState type
Environment category has no sub-items (depth-2 path), so currentPath[2] is undefined. Added null guard for panelKey before calling .replace().
- Replace flat changesSummary with ChangeDiff[] showing setting name, old value, and new value (e.g. "Theme : Dracula → Nord") - Add proper line breaks in prompt: unsaved changes header, diff list, and action prompt are now on separate lines - Add onChanged callback to all settings panels (Theme, Title Shape, Nanocoder Shape, Paste Threshold, Notifications, Auto-Compact, Sessions, Default Mode, Reasoning Traces, Web Search) - Use useRef accumulator to collect diffs across panel navigation - Fix: handle categories with no sub-items (Environment) in dirty state
# Conflicts: # source/app/components/settings-selector.tsx # source/commands/setup-config.tsx # source/commands/setup-mcp.tsx # source/commands/setup-providers.tsx # source/config/preferences.ts
vestige from LLM planning document
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Introduces a hierarchical /settings TUI with new panels (sessions, auto-compact, default mode, web search, tool approval, env view), adds global config read/write helpers, and marks several legacy commands as deprecated in favor of the new settings UI.
Changes:
- Added a path-based settings navigation system with category menus, leaf panels, and a Keep/Discard prompt for tracked changes.
- Added
config-writerhelpers to persist updates into globalagents.config.jsonunder thenanocoderkey. - Deprecated older setup/tune/ide commands and updated command registry descriptions and tests.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| source/hooks/useAppState.tsx | Adds settings return path state for wizard → settings navigation. |
| source/config/config-writer.ts | Adds sync helpers for reading/updating global agents.config.json. |
| source/commands/tune.ts | Updates tune command description to deprecated guidance. |
| source/commands/setup-providers.tsx | Marks providers wizard command deprecated; clarifies handler is intercepted. |
| source/commands/setup-mcp.tsx | Marks MCP wizard command deprecated; clarifies handler is intercepted. |
| source/commands/setup-config.tsx | Marks setup-config deprecated and logs a warning on use. |
| source/commands/settings.ts | Updates settings command description for broader scope. |
| source/commands/lazy-registry.ts | Updates descriptions with deprecation guidance and new settings framing. |
| source/commands/ide.tsx | Marks IDE command deprecated in description. |
| source/commands/ide.spec.tsx | Updates test expectations for deprecated description. |
| source/app/components/settings-web-search.tsx | Adds Web Search API key panel that writes to global config. |
| source/app/components/settings-tool-approval.tsx | Adds read-only tool auto-approval list panel. |
| source/app/components/settings-sessions.tsx | Adds sessions configuration panel persisted to global config. |
| source/app/components/settings-selector.tsx | Replaces flat menu with hierarchical categories + panel router + dirty tracking. |
| source/app/components/settings-selector.spec.tsx | Updates tests for new top-level settings menu categories. |
| source/app/components/settings-reasoning-traces.tsx | Adds reasoning traces default expand/collapse toggle panel. |
| source/app/components/settings-menu-types.ts | Adds shared types/constants for menu categories, paths, and items. |
| source/app/components/settings-keep-discard-prompt.tsx | Adds Keep/Discard prompt UI and ChangeDiff type. |
| source/app/components/settings-default-mode.tsx | Adds default CLI mode selector persisted to global config. |
| source/app/components/settings-auto-compact.tsx | Adds auto-compact settings panel persisted to global config. |
| docs/settings-menu-map.md | Documents settings menu mapping across config sources. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const [path, setPath] = useState<SettingsPath>(ROOT_PATH); | ||
| const [dirtyState, setDirtyState] = useState<DirtyState | null>(null); | ||
| // Accumulates change diffs as panels report them | ||
| const changesRef = useRef<ChangeDiff[]>([]); |
| const goBack = useCallback(() => { | ||
| setPath(currentPath => { | ||
| const newParent = parentPath(currentPath); | ||
|
|
||
| // If going back to root, check for dirty state | ||
| if (isRootPath(newParent) && !isRootPath(currentPath)) { | ||
| const categorySegment = currentPath[1] as SettingsCategory; | ||
| const changes = changesRef.current; | ||
|
|
||
| if (changes.length > 0) { | ||
| setDirtyState({ | ||
| isDirty: true, | ||
| category: categorySegment, | ||
| changes: [...changes], | ||
| }); | ||
| } | ||
| } else { | ||
| setDirtyState(null); | ||
| } | ||
|
|
||
| return newParent; | ||
| }); | ||
| }, []); | ||
|
|
||
| // Handle Keep/Discard | ||
| const handleKeep = useCallback(() => { | ||
| // Changes are already persisted by individual panels | ||
| changesRef.current = []; | ||
| setDirtyState(null); | ||
| setPath(ROOT_PATH); | ||
| }, []); | ||
|
|
||
| const handleDiscard = useCallback(() => { | ||
| // For Appearance settings that preview live, reload from preferences. | ||
| // For other categories, changes were already saved on apply. | ||
| changesRef.current = []; | ||
| setDirtyState(null); | ||
| setPath(ROOT_PATH); | ||
| }, []); |
| | Providers (list) | wizard | Providers | Configure Providers | ✅ | `agents.config.json` | Launches existing wizard | | ||
| | Copilot credentials | wizard | Providers | Copilot Login | ✅ | `agents.config.json` | Launches login flow | | ||
| | Codex credentials | wizard | Providers | Codex Login | ✅ | `agents.config.json` | Launches login flow | | ||
| | `alwaysAllow` (top-level) | list | Providers | Tool Auto-Approval | ✅ | `agents.config.json` | Read-only display | | ||
| | `nanocoderTools.alwaysAllow` | list | Providers | Tool Auto-Approval | ✅ | `agents.config.json` | Read-only display | | ||
| | MCP servers | wizard | MCPs | Configure MCP Servers | ✅ | `agents.config.json` | Launches existing wizard | | ||
| | `nanocoderTools.webSearch.apiKey` | text | Web Search | API Key | ✅ | `agents.config.json` | Masked input | | ||
| | `NANOCODER_*` env vars | read-only | Environment | (all) | ❌ | `process.env` | Read-only display | | ||
| | `tune.*` | wizard | Advanced | Tune Model | ✅ | `nanocoder-preferences.json` | Launches existing wizard | | ||
| | Config file paths | wizard | Advanced | Edit Config Files | ✅ | filesystem | Launches file picker | | ||
| | IDE connection | wizard | Advanced | Connect IDE | ✅ | `agents.config.json` | Launches existing wizard | |
| try { | ||
| const dir = dirname(configPath); | ||
| if (!existsSync(dir)) { | ||
| mkdirSync(dir, {recursive: true}); | ||
| } | ||
| writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); | ||
| } catch (error) { | ||
| logError(`Failed to write config update: ${String(error)}`); | ||
| } |
| setSaved(true); | ||
| setTimeout(() => { | ||
| setEditMode(false); | ||
| setInputValue(''); | ||
| setSaved(false); | ||
| }, 1500); |
| name: 'setup-config', | ||
| description: 'Open a configuration file in your editor', | ||
| description: | ||
| '[deprecated — use /settings → Advanced] Open a configuration file in your editor', | ||
| load: () => | ||
| import('@/commands/setup-config').then(m => m.setupConfigCommand), | ||
| }, |
| load: () => import('@/commands/tasks').then(m => m.tasksCommand), | ||
| }, | ||
| { | ||
| name: 'settings', | ||
| description: | ||
| 'Configure UI settings (theme, shapes, branding, paste threshold)', | ||
| 'Configure nanocoder settings (appearance, behavior, providers, MCPs, and more)', | ||
| load: () => import('@/commands/settings').then(m => m.settingsCommand), | ||
| }, | ||
| { | ||
| name: 'tune', | ||
| description: | ||
| 'Tune model settings (parameters, tool profiles, prompt, compaction)', | ||
| '[deprecated — use /settings → Advanced] Tune model settings (parameters, tool profiles, prompt, compaction)', | ||
| load: () => import('@/commands/tune').then(m => m.tuneCommand), | ||
| }, |
| // If dirty state is active, show the Keep/Discard prompt | ||
| if (dirtyState?.isDirty) { | ||
| return ( | ||
| <KeepDiscardPrompt | ||
| onKeep={handleKeep} | ||
| onDiscard={handleDiscard} | ||
| changes={dirtyState.changes} | ||
| /> | ||
| ); | ||
| } |
|
@grenkoca Thanks for tackling this huge settings reorganization. The overall direction looks great, and I really like the new hierarchical structure, breadcrumbs, and navigation flow. However, after reviewing the implementation and testing the workflows locally, I found a couple of regressions that I think need to be addressed before this can be merged. 1. Discard does not actually discard changesI was able to reproduce a case where settings are persisted immediately when selected, but choosing Discard later does not restore the previous value. Reproduction:
The newly selected value remains active. From what I can see, the panel persists changes immediately, while Suggested fix:
2. Deprecated commands currently lead to placeholder screensI tested the migration paths for several commands and found that users are directed into unimplemented screens. Example:
I was able to reproduce similar behavior for:
Suggested fix:
3. Missing test coverageI couldn't find coverage for:
Given the issues above, I think additional tests around these flows would be valuable before merge. 4. Config writer safety (recommendation)The new config writer currently writes directly to the target file. It may be worth considering an atomic write approach ( The overall direction of this PR is excellent and I think it will be a major UX improvement once these issues are addressed, but for now I'm requesting changes due to the Discard behavior and the dead-end migration paths. |
|
Hey @grenkoca - this looks awesome! I know you mentioned in the issue about getting support testing and finishing this issue - let me know if that's something you still want and @akramcodez and I can jump and wrapping too :) |
|
@akramcodez and @will-lamerton thanks for the thorough review! I know it's unfinished, but I figured it would be better to open a draft PR and get some help rather than let the changes sit uncommitted on my end. Perfect is the enemy of good, and y'all are the experts :) @akramcodez: regarding your specific points
Regarding point 2, here are some good examples:
Ultimately a good solution would probably just be to extend Let me know what direction you want to go! |
…rotects nanocoder-preferences.json from corruption/interrupted writes. Also fixed loadSessionConfig from reading from nanocoder-preferences.json instead of agents.config.json
…s that nothing hits the disk until is confirmed
Description
Draft implementation of #471, which reorganizes orphaned settings into a breadcrumb menu
Type of Change
Testing
Automated Tests
.spec.ts/tsxfilespnpm test:allcompletes successfully)Manual Testing
Checklist