Skip to content

feat(web): settings modal shell + gear button (deep-linkable via ?settings)#253

Open
mgabor3141 wants to merge 2 commits into
feat/local-host-multihostfrom
feat/settings-shell
Open

feat(web): settings modal shell + gear button (deep-linkable via ?settings)#253
mgabor3141 wants to merge 2 commits into
feat/local-host-multihostfrom
feat/settings-shell

Conversation

@mgabor3141
Copy link
Copy Markdown
Contributor

Closes #249.

Stacked on #248 (feat/local-host-multihost) — review/merge that first; this targets that branch and will be rebased onto main once #248 lands. The diff below is settings-shell only.

Summary

First slice of the home/settings restructure (#249): introduce a deep-linkable Settings modal and a gear button, replacing the standalone "Manage projects" modal. Shell + entry point only — the modal's content is unchanged (still today's add/discovery UI); the tab bar and the configured-project manage-list arrive in later slices (#250/#251).

Changes

  • manage-projects.tsxsettings.tsx, export ManageProjectsModalSettingsModal. Content/behavior otherwise unchanged.
  • URL-driven open state: settings is encoded as a ?settings query param read from loc.query, not the path. The path-derived view is untouched, so opening settings over a live session keeps the terminal/websocket mounted. Open pushes a history entry (back closes); close replaces. Open/close preserve any other query params (only settings is touched).
  • Gear button in the sidebar header opposite the logo, on desktop and mobile (within the drawer); aria-label/title "Settings". New IconSettings (sliders glyph).
  • Empty-state launch button relocated out of the header into the sidebar body (.sidebar-empty-launch), so the header is just logo + gear when you have no projects.
  • Sidebar onManageProjects prop → onOpenSettings; the "Add a project" hint link and the gear both open settings.

Testing

  • 424 web tests pass; lint/build clean.
  • Verified via mock mode: gear opens ?settings=projects over the preserved home view; deep-linking ?settings=projects opens directly; close strips the param; unrelated query params survive open/close.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 29, 2026

Greptile Summary

Introduces a URL-driven Settings modal (?settings=<tab>) and a gear button in the sidebar header, replacing the local-state-driven "Manage Projects" modal. The path-derived view is left untouched so a live terminal stays mounted while settings is open; open pushes a history entry, close replaces it.

  • manage-projects.tsx is renamed to settings.tsx with the export renamed to SettingsModal; modal content is unchanged.
  • openSettings/closeSettings callbacks in App manipulate only the settings query param while preserving all other params; settingsOpen is derived from loc.query.settings.
  • The sidebar gains a permanent gear button (IconSettings) and the empty-state LaunchButton moves from the header into the scroll body.

Confidence Score: 4/5

Safe to merge; changes are well-scoped to the settings entry point with no logic mutations to the modal content itself.

The URL-driven open/close logic is clean and the component rename is mechanical. The one issue is that clicking the gear while the settings modal is already visible (easy on desktop) pushes a redundant history entry, making the back button land on a duplicate settings URL rather than the previous page. It is reproducible but non-blocking.

apps/gmux-web/src/main.tsx — the openSettings history-push logic

Important Files Changed

Filename Overview
apps/gmux-web/src/main.tsx Replaces local manageProjectsOpen state with URL-driven settingsOpen derived from loc.query.settings; introduces openSettings/closeSettings callbacks; minor duplicate-push edge case when gear is clicked while modal is already open
apps/gmux-web/src/settings.tsx Rename from manage-projects.tsx + export name SettingsModal; content and logic unchanged; comments updated to reflect future tab-bar slices
apps/gmux-web/src/sidebar.tsx Adds IconSettings SVG, replaces onManageProjects prop with onOpenSettings, moves LaunchButton from header into sidebar body for empty state, adds sidebar-settings-btn gear button
apps/gmux-web/src/styles.css Adds CSS for .sidebar-settings-btn (gear button) and .sidebar-empty-launch (repositioned LaunchButton container); clean additions with no apparent conflicts

Sequence Diagram

sequenceDiagram
    participant User
    participant Sidebar
    participant App
    participant Browser as Browser History
    participant SettingsModal

    User->>Sidebar: clicks gear button
    Sidebar->>App: onOpenSettings callback
    App->>Browser: "pushState to ?settings=projects"
    App->>SettingsModal: "open=true"

    User->>SettingsModal: Escape or backdrop click
    SettingsModal->>App: onClose callback
    App->>Browser: replaceState strips ?settings
    App->>SettingsModal: "open=false"

    User->>Browser: presses Back
    Browser->>App: popstate to previous URL
    App->>SettingsModal: "open=false"
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/gmux-web/src/main.tsx:372-376
**Duplicate history push when settings already open**

`openSettings` unconditionally calls `loc.route(...)`, which does a `history.pushState`, even when `?settings` is already present in the URL. On desktop the gear button is always visible, so clicking it while the modal is open pushes an identical history entry — pressing back then lands on `?settings=projects` again, and only a second back actually closes the modal. A quick guard (or a `replace` when already open) prevents the stale entry.

```suggestion
  const openSettings = useCallback((tab = 'projects') => {
    const params = new URLSearchParams(location.search)
    // If the requested tab is already active, replace rather than push so
    // the back button doesn't land on a duplicate settings entry.
    const alreadyOpen = params.get('settings') === tab
    params.set('settings', tab)
    loc.route(`${loc.path}?${params.toString()}`, alreadyOpen)
  }, [loc])
```

Reviews (1): Last reviewed commit: "feat(web): settings modal shell + gear b..." | Re-trigger Greptile

Comment on lines +372 to +376
const openSettings = useCallback((tab = 'projects') => {
const params = new URLSearchParams(location.search)
params.set('settings', tab)
loc.route(`${loc.path}?${params.toString()}`)
}, [loc])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Duplicate history push when settings already open

openSettings unconditionally calls loc.route(...), which does a history.pushState, even when ?settings is already present in the URL. On desktop the gear button is always visible, so clicking it while the modal is open pushes an identical history entry — pressing back then lands on ?settings=projects again, and only a second back actually closes the modal. A quick guard (or a replace when already open) prevents the stale entry.

Suggested change
const openSettings = useCallback((tab = 'projects') => {
const params = new URLSearchParams(location.search)
params.set('settings', tab)
loc.route(`${loc.path}?${params.toString()}`)
}, [loc])
const openSettings = useCallback((tab = 'projects') => {
const params = new URLSearchParams(location.search)
// If the requested tab is already active, replace rather than push so
// the back button doesn't land on a duplicate settings entry.
const alreadyOpen = params.get('settings') === tab
params.set('settings', tab)
loc.route(`${loc.path}?${params.toString()}`, alreadyOpen)
}, [loc])
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/gmux-web/src/main.tsx
Line: 372-376

Comment:
**Duplicate history push when settings already open**

`openSettings` unconditionally calls `loc.route(...)`, which does a `history.pushState`, even when `?settings` is already present in the URL. On desktop the gear button is always visible, so clicking it while the modal is open pushes an identical history entry — pressing back then lands on `?settings=projects` again, and only a second back actually closes the modal. A quick guard (or a `replace` when already open) prevents the stale entry.

```suggestion
  const openSettings = useCallback((tab = 'projects') => {
    const params = new URLSearchParams(location.search)
    // If the requested tab is already active, replace rather than push so
    // the back button doesn't land on a duplicate settings entry.
    const alreadyOpen = params.get('settings') === tab
    params.set('settings', tab)
    loc.route(`${loc.path}?${params.toString()}`, alreadyOpen)
  }, [loc])
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: openSettings now replaces instead of pushing when the requested tab is already active (replace || alreadyActive), so clicking the gear while the modal is open no longer stacks a duplicate history entry. (The replace param itself lands in the hosts-tab PR up the stack, where tab switches also replace.)

@github-actions
Copy link
Copy Markdown
Contributor

Try this PR

curl -sSfL https://gmux.app/install-pr.sh | sh -s -- 253

Built from 586887a — feat(web): settings modal shell + gear button (deep-linkable via ?settings)
Requires GitHub CLI with auth. Artifacts expire after 7 days.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant