Skip to content

feat(ui): prompt to save view preferences on quit#468

Open
benvinegar wants to merge 1 commit into
mainfrom
feat/save-view-preferences-on-quit
Open

feat(ui): prompt to save view preferences on quit#468
benvinegar wants to merge 1 commit into
mainfrom
feat/save-view-preferences-on-quit

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Summary

  • prompt on quit when local view preferences changed
  • save accepted preferences to the config file that will affect the next launch
  • list only changed preferences in the prompt

Verification

  • bun run typecheck
  • bun test src/core/config.test.ts src/ui/AppHost.interactions.test.tsx --test-name-pattern 'config persistence|merges global|defaults unspecified|quit prompt'\n\nThis PR description was generated by Pi using OpenAI GPT-5

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a quit-time prompt that detects in-session view-preference changes (theme, layout, line numbers, wrapping, hunk headers, agent notes, copy decorations), shows a diff of what changed, and optionally persists them to the resolved config file via a hand-rolled TOML upsert that preserves tables and comments.

  • saveGlobalViewPreferences in config.ts writes only the changed preference keys by upserting top-level TOML entries while leaving [custom_theme] sections intact; viewPreferencesConfigPath is wired through startup and bootstrap so the correct file (repo or global) is targeted.
  • App.tsx tracks initialViewPreferencesRef against currentViewPreferences and gates the quit path through a ModalFrame dialog with Save/Discard/Cancel choices backed by new keyboard shortcuts in useAppKeyboardShortcuts.
  • Tests cover both the TOML upsert logic and the full interaction flow (prompt-and-discard, prompt-and-save) with real temp-dir file I/O.

Confidence Score: 4/5

Safe to merge — the TOML upsert is well-tested and the quit flow is gated correctly; the main rough edge is the confirmation notice that fires and is discarded before the terminal can render it.

The core save path (TOML manipulation, config-path resolution, keyboard routing) is solid and covered by unit and interaction tests. Two cosmetic issues stand out: a misplaced JSDoc comment that orphans resolveConfiguredCliInput's docs, and the session notice in saveViewPreferencesAndQuit being emitted synchronously before onQuit() tears down the component, so the confirmation is almost certainly never rendered. Neither blocks correctness — preferences are written to disk before quit — but the UX confirmation is dead code in its current form.

src/ui/App.tsx (saveViewPreferencesAndQuit notice timing) and src/core/config.ts (orphaned JSDoc)

Important Files Changed

Filename Overview
src/core/config.ts Adds TOML upsert logic and saveGlobalViewPreferences export; has a misplaced JSDoc comment that orphans resolveConfiguredCliInput's documentation
src/ui/App.tsx Adds quit-prompt modal and view-preference diffing; session notice after save fires before onQuit() allows a re-render so the confirmation is never displayed
src/ui/hooks/useAppKeyboardShortcuts.ts Adds save-config-prompt keyboard handling; undocumented [s] shortcut is not surfaced in the modal UI
src/core/config.test.ts New config-persistence test verifies TOML upsert preserves sections; resolution tests extended with viewPreferencesConfigPath assertions
src/ui/AppHost.interactions.test.tsx Two new interaction tests cover the quit-prompt flow (prompt-and-discard, prompt-and-save); good coverage with real temp-dir file I/O

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant App
    participant useAppKeyboardShortcuts
    participant saveGlobalViewPreferences
    participant ConfigFile

    User->>App: press q (quit)
    App->>App: requestQuit()
    alt hasUnsavedViewPreferences
        App->>App: setShowHelp(false)
        App->>App: setSaveConfigPromptOpen(true)
        App-->>User: show "Save view preferences?" modal
        alt User presses Enter / s
            useAppKeyboardShortcuts->>App: saveViewPreferencesAndQuit()
            App->>saveGlobalViewPreferences: preferences + configPath
            saveGlobalViewPreferences->>ConfigFile: upsertTopLevelTomlValue (per key)
            ConfigFile-->>saveGlobalViewPreferences: written
            saveGlobalViewPreferences-->>App: configPath
            App->>App: showSessionNotice(...)
            App->>App: onQuit()
        else User presses d / n
            useAppKeyboardShortcuts->>App: discardViewPreferencesAndQuit()
            App->>App: setSaveConfigPromptOpen(false)
            App->>App: onQuit()
        else User presses Esc
            useAppKeyboardShortcuts->>App: closeSaveConfigPrompt()
            App->>App: setSaveConfigPromptOpen(false)
        end
    else no unsaved changes
        App->>App: onQuit()
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant App
    participant useAppKeyboardShortcuts
    participant saveGlobalViewPreferences
    participant ConfigFile

    User->>App: press q (quit)
    App->>App: requestQuit()
    alt hasUnsavedViewPreferences
        App->>App: setShowHelp(false)
        App->>App: setSaveConfigPromptOpen(true)
        App-->>User: show "Save view preferences?" modal
        alt User presses Enter / s
            useAppKeyboardShortcuts->>App: saveViewPreferencesAndQuit()
            App->>saveGlobalViewPreferences: preferences + configPath
            saveGlobalViewPreferences->>ConfigFile: upsertTopLevelTomlValue (per key)
            ConfigFile-->>saveGlobalViewPreferences: written
            saveGlobalViewPreferences-->>App: configPath
            App->>App: showSessionNotice(...)
            App->>App: onQuit()
        else User presses d / n
            useAppKeyboardShortcuts->>App: discardViewPreferencesAndQuit()
            App->>App: setSaveConfigPromptOpen(false)
            App->>App: onQuit()
        else User presses Esc
            useAppKeyboardShortcuts->>App: closeSaveConfigPrompt()
            App->>App: setSaveConfigPromptOpen(false)
        end
    else no unsaved changes
        App->>App: onQuit()
    end
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/core/config.ts:360-361
**Stray JSDoc steals documentation from `resolveConfiguredCliInput`**

The original `/** Resolve CLI input against global and repo-local config files. */` comment was the JSDoc for `resolveConfiguredCliInput`, but `saveGlobalViewPreferences` was inserted immediately after it. Now two JSDoc blocks precede `saveGlobalViewPreferences`, the first of which is incorrect, and `resolveConfiguredCliInput` (line 392) has no JSDoc at all. Most tooling uses the last preceding block, so generated docs will show the right text for `saveGlobalViewPreferences`, but `resolveConfiguredCliInput` silently loses its documentation. Move the `/** Resolve CLI input... */` comment down to sit directly above `resolveConfiguredCliInput`.

### Issue 2 of 3
src/ui/App.tsx:709-722
**Session notice is never visible — app quits before the notice renders**

`showSessionNotice(...)` schedules a React state update, but `onQuit()` is called synchronously in the same callback before any re-render. If `onQuit()` tears down the terminal (or unmounts the component tree) before the next paint, the "Saved view preferences to …" message is never displayed. The save itself succeeds, but the user gets no confirmation feedback. Consider calling `onQuit()` inside a `setTimeout` or a `useEffect` that triggers after the notice has rendered, similar to how other deferred-then-quit flows are handled in the codebase.

### Issue 3 of 3
src/ui/hooks/useAppKeyboardShortcuts.ts:274-282
**Undocumented `[s]` shortcut to save — the modal UI only shows `[Enter]`**

The keyboard handler accepts both `Enter` and `s` to trigger `saveViewPreferencesAndQuit`, but the modal in `App.tsx` renders only `[Enter] Save and quit` as the hint. A user who presses `s` will save-and-quit without any visual cue that `s` is a valid binding, while a user who looks for an `s` shortcut (consistent with other modals in the app) will not find the hint. Either drop the `s` binding or add it to the modal label.

Reviews (1): Last reviewed commit: "feat(ui): prompt to save view preference..." | Re-trigger Greptile

Comment thread src/core/config.ts Outdated
Comment on lines +360 to +361
/** Resolve CLI input against global and repo-local config files. */
/** Persist accepted in-app view preferences to the user-level Hunk config file. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Stray JSDoc steals documentation from resolveConfiguredCliInput

The original /** Resolve CLI input against global and repo-local config files. */ comment was the JSDoc for resolveConfiguredCliInput, but saveGlobalViewPreferences was inserted immediately after it. Now two JSDoc blocks precede saveGlobalViewPreferences, the first of which is incorrect, and resolveConfiguredCliInput (line 392) has no JSDoc at all. Most tooling uses the last preceding block, so generated docs will show the right text for saveGlobalViewPreferences, but resolveConfiguredCliInput silently loses its documentation. Move the /** Resolve CLI input... */ comment down to sit directly above resolveConfiguredCliInput.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/config.ts
Line: 360-361

Comment:
**Stray JSDoc steals documentation from `resolveConfiguredCliInput`**

The original `/** Resolve CLI input against global and repo-local config files. */` comment was the JSDoc for `resolveConfiguredCliInput`, but `saveGlobalViewPreferences` was inserted immediately after it. Now two JSDoc blocks precede `saveGlobalViewPreferences`, the first of which is incorrect, and `resolveConfiguredCliInput` (line 392) has no JSDoc at all. Most tooling uses the last preceding block, so generated docs will show the right text for `saveGlobalViewPreferences`, but `resolveConfiguredCliInput` silently loses its documentation. Move the `/** Resolve CLI input... */` comment down to sit directly above `resolveConfiguredCliInput`.

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Moved the stale JSDoc back above resolveConfiguredCliInput and left the persistence helper with its own accurate comment.

This comment was generated by Pi using OpenAI GPT-5

Comment thread src/ui/App.tsx
Comment on lines +709 to +722
const saveViewPreferencesAndQuit = useCallback(() => {
try {
const configPath = saveGlobalViewPreferences(currentViewPreferences, {
configPath: bootstrap.viewPreferencesConfigPath,
});
initialViewPreferencesRef.current = currentViewPreferences;
showSessionNotice(`Saved view preferences to ${configPath}`);
onQuit();
} catch (error) {
showSessionNotice(
error instanceof Error ? error.message : "Failed to save view preferences.",
);
}
}, [bootstrap.viewPreferencesConfigPath, currentViewPreferences, onQuit, showSessionNotice]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Session notice is never visible — app quits before the notice renders

showSessionNotice(...) schedules a React state update, but onQuit() is called synchronously in the same callback before any re-render. If onQuit() tears down the terminal (or unmounts the component tree) before the next paint, the "Saved view preferences to …" message is never displayed. The save itself succeeds, but the user gets no confirmation feedback. Consider calling onQuit() inside a setTimeout or a useEffect that triggers after the notice has rendered, similar to how other deferred-then-quit flows are handled in the codebase.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/App.tsx
Line: 709-722

Comment:
**Session notice is never visible — app quits before the notice renders**

`showSessionNotice(...)` schedules a React state update, but `onQuit()` is called synchronously in the same callback before any re-render. If `onQuit()` tears down the terminal (or unmounts the component tree) before the next paint, the "Saved view preferences to …" message is never displayed. The save itself succeeds, but the user gets no confirmation feedback. Consider calling `onQuit()` inside a `setTimeout` or a `useEffect` that triggers after the notice has rendered, similar to how other deferred-then-quit flows are handled in the codebase.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated the save path to defer onQuit() briefly after showing the saved-preferences notice, and adjusted the regression test to cover the delayed quit.

This comment was generated by Pi using OpenAI GPT-5

Comment on lines +274 to +282
if (!saveConfigPromptOpenRef.current) {
return false;
}

consumeKey(key);
if (key.name === "return" || key.name === "enter" || key.name === "s" || key.sequence === "s") {
saveViewPreferencesAndQuit();
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Undocumented [s] shortcut to save — the modal UI only shows [Enter]

The keyboard handler accepts both Enter and s to trigger saveViewPreferencesAndQuit, but the modal in App.tsx renders only [Enter] Save and quit as the hint. A user who presses s will save-and-quit without any visual cue that s is a valid binding, while a user who looks for an s shortcut (consistent with other modals in the app) will not find the hint. Either drop the s binding or add it to the modal label.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/hooks/useAppKeyboardShortcuts.ts
Line: 274-282

Comment:
**Undocumented `[s]` shortcut to save — the modal UI only shows `[Enter]`**

The keyboard handler accepts both `Enter` and `s` to trigger `saveViewPreferencesAndQuit`, but the modal in `App.tsx` renders only `[Enter] Save and quit` as the hint. A user who presses `s` will save-and-quit without any visual cue that `s` is a valid binding, while a user who looks for an `s` shortcut (consistent with other modals in the app) will not find the hint. Either drop the `s` binding or add it to the modal label.

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added the s shortcut to the modal hint so the displayed binding matches the keyboard handler.

This comment was generated by Pi using OpenAI GPT-5

@benvinegar benvinegar force-pushed the feat/save-view-preferences-on-quit branch from f480c95 to f097d53 Compare June 21, 2026 01:20
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