Skip to content

Implement command palette, tab management, and UI enhancements#3

Open
parazeeknova wants to merge 14 commits into
mainfrom
dev
Open

Implement command palette, tab management, and UI enhancements#3
parazeeknova wants to merge 14 commits into
mainfrom
dev

Conversation

@parazeeknova
Copy link
Copy Markdown
Owner

@parazeeknova parazeeknova commented May 21, 2026

This pull request introduces significant enhancements to the application's Git integration, session persistence, and user interface. The most notable changes include the addition of fetch, pull, and push operations for repositories, improved session management to restore and persist user state, and the integration of a command palette for quick actions. Additionally, the repository manager view has been enriched with more detailed information, and the Tag model now includes author and timestamp fields.

Git Operations and Models:

  • Added fetch, pull, and push methods to the GitRepo struct, enabling the application to interact with remote repositories directly from the UI. These operations are now accessible via the command palette. [1] [2]
  • Enhanced the Tag struct to include author and timestamp fields, allowing for richer tag metadata throughout the UI and repository manager. [1] [2] [3]

Session Persistence and Restoration:

  • Implemented session persistence by saving and restoring the application state, including the active repository and UI state, across restarts. This is handled via new methods like persist_session and restore_active_repo_from_state. [1] [2] [3] [4]

User Interface and Command Palette:

  • Integrated a command palette and quick launch actions, allowing users to perform common operations (open repo, fetch, pull, push, etc.) via shortcuts or the toolbar. The palette is accessible through a dedicated shortcut and toolbar button. [1] [2] [3]
  • Updated UI event handling to support tab activation/closure and recent repo selection, ensuring the correct repository is loaded and session state is updated accordingly. [1] [2]

Repository Manager Enhancements:

  • Improved the repository manager view to display detailed information about branches, tags (now with author and date), remotes, and recent commits, providing a more comprehensive overview of repository state.

Dependency and Version Updates:

  • Updated the Cargo.toml to enable persistence in the eframe dependency and bumped the package version to 0.0.48.

These changes collectively improve the usability, reliability, and feature set of the application, making it more robust and user-friendly for interacting with Git repositories.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added command palette (Cmd+K) for quick access to repository operations and navigation
    • Added repository manager with recent repositories sidebar and detailed repository information display
    • Added fetch, pull, and push operations for remote synchronization
    • Implemented session persistence to restore active repository on startup
  • Improvements

    • Enhanced tab bar and titlebar UI with improved layout and interaction
    • Added relative time formatting for recent repository timestamps
    • Improved sidebar and toolbar layouts

Review Change Stack

- New command_palette.rs with searchable command modal
- Commands: Open Repo, Exit, Logs, Fetch, Pull, Push, Stage All, Discard All, Create Branch
- Ctrl+K / Cmd+K keyboard shortcut to open
- Quick Launch toolbar button wired to open palette
- Commands filtered by repo availability
- Keyboard navigation (arrows, Enter, Escape)
- Added fetch/pull/push methods to GitRepo
Modal was only rendering for one frame because show() was only called
on the trigger event. Added show_command_palette flag to persist the
modal across frames until user selects an action or dismisses it.
- Toolbar hover effect with subtle highlight
- Command palette styled input bar matching commit panel
- Ctrl+K / Cmd+K shortcut hint in search bar
- Per-command shortcut hints (Ctrl+O, Ctrl+Q, Ctrl+Shift+L)
- OS-aware shortcut display (macOS symbols vs Windows/Linux text)
- Dynamic palette height adapting to filtered results count
- Auto-scroll to selected item on arrow key navigation
- Global shortcuts for Open Repo (Ctrl+O), Exit (Ctrl+Q), Logs (Ctrl+Shift+L)
- Fixed modal persistence with show_command_palette flag
- Fixed toolbar button click detection with Sense::click
- AppSession load/save for persistent state across launches
- Tab switching with ActivateTab action
- Auto-restore active repo on app launch
- Persist session on repo open and tab switch
- Updated tabbar UI with tab management
- Nuke feedback button from toolbar
- Stash button uses same pattern as other toolbar buttons
- Tab bar: fixed max width, git icon, parent folder name, truncated path
- Titlebar: remove Palimpsest/Local-first text from Help menu
- Help menu links: non-blue, no underline, with icons and visit arrows
- File menu: icons for Open, Recents, Exit
- Recents submenu: heading, folder icons per repo
- Window menu: icon left, checkbox right, max width
- Debug menu: dropdown with Open Logs option and terminal icon
- Menu buttons highlight when open (connected to dropdown)
- All dropdowns consistent 200px max width
Copilot AI review requested due to automatic review settings May 21, 2026 20:25
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Walkthrough

This PR introduces repository session persistence, a command palette quick-launcher, and a new repository manager UI that displays recent and available repositories. Git operations (fetch, pull, push) are added to the repository API with enriched tag metadata (author, timestamp). The app now persists recent repositories with timestamps and restores the last active repository on startup. Multi-tab support is refactored to be state-driven, and the UI conditionally switches between a "manager" sidebar/body mode (when no repo is active) and the normal editing interface.

Changes

Repository Manager & Session Persistence

Layer / File(s) Summary
Git tag enrichment and remote operations
src/git/models.rs, src/git/repo.rs
Tag struct adds author and timestamp fields; tag-fetching extracts author name/time from git2 tag targets. New GitRepo::fetch(), GitRepo::pull(), and GitRepo::push() methods enable remote synchronization.
Session persistence and recent repositories tracking
src/state/mod.rs
AppSession persists state via JSON using temp-file-rename strategy with version normalization. RecentRepo tracks path and last_opened timestamp. AppState gains manager-related fields. Store initialization now takes AppSession, recent-repo ordering/truncation, and tab state transitions are refactored into helper methods. Test suite updated for new structures and behaviors.
Command palette quick-launch UI
src/ui/command_palette.rs
New egui-based command palette with keyboard-driven selection (Escape/ArrowUp/ArrowDown/Enter), query filtering, and platform-specific shortcut labels. Returns QuickLaunchAction enum for open repo, fetch/pull/push, stage/discard, exit, and open logs.
Repository manager sidebar, body, and utilities
src/ui/repo_manager.rs, src/ui/repo_manager_sidebar.rs, src/ui/repo_manager_body.rs
Sidebar displays recent and all repositories with collapsible sections and relative-time labels. Body renders selected repo details: top bar with open/explore actions, stats panel (files, commits, dates, remotes), and collapsible branches/tags sections with commits. Utilities provide relative time formatting, GitHub URL detection, and semantic version parsing. Avatars use deterministic colors and initials.
Tab bar refactoring to state-driven model
src/ui/tabbar.rs
Tab bar now derives from AppState open_tabs and active_tab instead of single repo_name. Returns TabAction (Open, Activate, Close). Added tab truncation with character-width budgeting and filesystem helpers to derive display name/location from path.
Sidebar, titlebar, toolbar, and body updates for state-driven flow
src/ui/sidebar.rs, src/ui/titlebar.rs, src/ui/toolbar.rs, src/ui/body.rs
Sidebar early-returns with centered logo when no repo is selected; titlebar accepts RecentRepo slice and formats recent entries with timestamps; toolbar refactored to track and return quick-launch click state; body renders centered logo for empty commit list.
App initialization, session persistence, and mode orchestration
src/main.rs
App loads persisted session on startup and restores active repo. Session is persisted after repo open/close, tab changes, and palette/button visibility changes. Conditional "manager mode" UI renders repo manager sidebar/body when no current repo exists. Command palette and quick-launch actions (fetch/pull/push/stage/discard) are routed and dispatched. Drop trait ensures session persistence on shutdown.
Version bump and module exports
Cargo.toml, src/ui/mod.rs
Version bumped from 0.0.29 to 0.0.48. eframe dependency enables persistence feature. UI module exports new submodules: command_palette, repo_manager, repo_manager_body, repo_manager_sidebar.

Sequence Diagram(s)

sequenceDiagram
  participant User as User/OS
  participant App as PalimpsestApp
  participant Palette as CommandPalette
  participant Manager as ManagerUI
  participant Store as AppStore
  User->>App: Launch
  App->>App: new()
  App->>App: load persisted session
  App->>Store: create_store(session)
  App->>App: restore_active_repo_from_state()
  User->>Palette: Press Cmd/Ctrl-K
  App->>Palette: check_shortcut()
  Palette-->>App: show modal
  User->>Palette: Select action
  Palette-->>App: Action(QuickLaunchAction)
  App->>App: handle_quick_launch_action()
  App->>Store: dispatch action (fetch/pull/push/etc)
  App->>App: persist_session()
  Note over App: Manager mode when no current repo
  App->>Manager: show sidebar+body
  User->>Manager: Select repo
  App->>Store: SelectManagerRepo
  App->>Manager: fetch_manager_details()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • parazeeknova/palimpsest#2: Updates the Tag struct with author and timestamp fields in src/git/models.rs and src/git/repo.rs tag-fetching logic to populate these new metadata fields.

Poem

🐰 A Rabbit's Ode to Repo Palimpsests

With manager's keen eye and quick palette bright,
Sessions persist through the digital night.
Tabs that remember, repos that stay—
A warren of work, saved every day! 🏔️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly summarizes the main changes: adding a command palette, tab management, and UI enhancements, all of which are prominently featured throughout the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a command palette and richer “workspace” UX on top of the existing egui UI, including tabbed repo switching and persisted session state. It also introduces new Git remote operations (fetch/pull/push) and extends tag metadata to support richer repository-manager views.

Changes:

  • Add command palette + global shortcuts and wire actions into the app loop.
  • Implement tab management and session persistence (open tabs, active tab, recents, window button visibility).
  • Add repository manager UI (sidebar + details body) and enrich Git tag metadata (author/timestamp) plus new remote ops.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/ui/toolbar.rs Returns a “quick launch” click signal; adjusts center content; reworks stash UI.
src/ui/titlebar.rs Enhances menus (recents w/ relative time, help links) and recent-repo model usage.
src/ui/tabbar.rs Implements multi-tab UI with activation/close/open actions and truncation logic.
src/ui/sidebar.rs Adds “no repo” placeholder and simplifies branch/remote/tag rendering flow.
src/ui/repo_manager.rs Adds helpers (relative time, GitHub URL helpers, tag version parsing) + tests.
src/ui/repo_manager_sidebar.rs Adds repository manager sidebar (recents/repos lists, selection).
src/ui/repo_manager_body.rs Adds repository manager detail view (stats/branches/tags/commits panels).
src/ui/mod.rs Exposes new UI modules.
src/ui/command_palette.rs Implements modal command palette, filtering, and shortcut handling.
src/ui/body.rs Adds placeholder logo when no commits are cached.
src/state/mod.rs Introduces persisted AppSession, tab state in AppState, and manager state/actions.
src/main.rs Wires session load/save, command palette, tab actions, and manager UI into the app.
src/git/repo.rs Adds tag author/timestamp extraction and new fetch/pull/push methods.
src/git/models.rs Extends Tag model with author and timestamp.
Cargo.toml Enables eframe persistence feature and bumps crate version.
Cargo.lock Updates lockfile for new dependency graph.
Comments suppressed due to low confidence (3)

src/git/repo.rs:598

  • Same issue as fetch: remotes.get(0)? is an Option and will not compile in a Result-returning function. Use remotes.get(0).ok_or_else(...) (and consider preferring a named remote like origin).
        let remotes = self.repo.remotes()?;
        let remote_name = remotes
            .get(0)?
            .ok_or_else(|| GitError::Git("No remotes configured".to_string()))?;
        let branch_name = self.head_branch()?;

src/git/repo.rs:624

  • Same issue as fetch: remotes.get(0)? is an Option and will not compile in a Result-returning function.
        let remotes = self.repo.remotes()?;
        let remote_name = remotes
            .get(0)?
            .ok_or_else(|| GitError::Git("No remotes configured".to_string()))?;
        let branch_name = self.head_branch()?;

src/ui/repo_manager_sidebar.rs:220

  • Same persistent-ID collision issue as above: ("manager_repo_recent", name) is not unique if multiple recents share the same folder name. Prefer using the full path (or the loop index) when constructing the persistent ID.
    let response = ui.interact(
        row,
        ui.make_persistent_id(("manager_repo_recent", name)),
        egui::Sense::click(),
    );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ui/toolbar.rs
Comment on lines +106 to +113
let stash_clicked = toolbar_button(ui, ACTION_WIDTH, STACK, "Stash", Some(CARET_DOWN));
if stash_clicked {
ui.menu_button(egui::RichText::new("").size(1.0), |ui| {
drop(ui.button("Stash changes"));
drop(ui.button("Apply stash"));
drop(ui.button("Pop stash"));
});
}
Comment thread src/git/repo.rs
Comment on lines +584 to +588
let remotes = self.repo.remotes()?;
let remote_name = remotes
.get(0)?
.ok_or_else(|| GitError::Git("No remotes configured".to_string()))?;
let mut remote = self.repo.find_remote(remote_name)?;
Comment thread src/git/repo.rs
Comment on lines +606 to +610
let ancestor = self.repo.merge_base(local_commit.id(), fetch_commit.id())?;
if ancestor == fetch_commit.id() {
return Ok(());
}
let tree = self.repo.find_commit(fetch_commit.id())?.tree()?;
Comment thread src/git/repo.rs
Comment on lines +611 to +616
let mut checkout_opts = git2::build::CheckoutBuilder::new();
checkout_opts.force();
self.repo
.checkout_tree(tree.as_object(), Some(&mut checkout_opts))?;
local_ref.set_target(fetch_commit.id(), "pull: Fast-forward")?;
Ok(())
Comment thread src/ui/command_palette.rs
TERMINAL_WINDOW, TRASH,
};

const COMMAND_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::K);
Comment thread src/main.rs
Comment on lines +540 to +543
if ctx.input_mut(|i| i.consume_shortcut(&exit_shortcut)) {
tracing::info!("Exiting app via shortcut");
std::process::exit(0);
}
Comment thread src/state/mod.rs
Comment on lines +140 to +143
if let Err(error) = fs::rename(&temp_path, &path) {
tracing::warn!(from = %temp_path.display(), to = %path.display(), error = %error, "Failed to commit session file");
let _ = fs::remove_file(&temp_path);
}
Comment on lines +175 to +179
let response = ui.interact(
row,
ui.make_persistent_id(("manager_repo", name)),
egui::Sense::click(),
);
Comment thread src/ui/body.rs
Comment on lines +561 to +563
ui.put(logo_rect, logo);
return;
}
Comment thread src/ui/tabbar.rs
Comment on lines +182 to +186
if tab.closeable {
ui.painter().text(
close_rect.center(),
egui::Align2::CENTER_CENTER,
X,
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/git/repo.rs`:
- Around line 601-616: In pull(), current logic fetches into FETCH_HEAD and
unconditionally force-checkouts and moves refs which can update the wrong branch
or discard divergent history; change the fetch call to fetch the specific remote
branch ref (use a refspec like
format!("refs/heads/{branch_name}:refs/remotes/{remote_name}/{branch_name}") or
equivalent) and then resolve the fetched commit from that remote-tracking ref
instead of FETCH_HEAD (replace uses of fetch_head/fetch_commit with the resolved
remote ref), and before modifying local_ref ensure you only fast-forward when
merge_base(local_commit, remote_commit) == local_commit; if histories diverge
(merge_base is neither), do not force-checkout or set_target—return an error or
require an explicit merge/rebase flow instead; keep checkout_tree/set_target
only for the safe fast-forward path.
- Around line 130-135: The tags() logic peels tag OIDs to commits and always
uses target.author()/author.when(), which misses annotated tag metadata; update
tags() to detect annotated tag objects first (e.g., try to peel/find the tag
object for the OID or use Object::peel_to_tag / repo.find_tag), and when a tag
object exists use its tagger().name() and tagger().when() for Tag.author and
Tag.timestamp, falling back to target.author() / author.when() for lightweight
tags or when tagger is absent; update the code paths constructing Tag (the code
building name, target_hash, author, timestamp) to prefer tag.tagger() metadata.

In `@src/main.rs`:
- Around line 540-543: The Cmd/Ctrl+Q shortcut currently calls
std::process::exit(0) and bypasses PalimpsestApp cleanup; change the shortcut
handling in main.rs (the ctx.input_mut(...) branch that checks
consume_shortcut(&exit_shortcut)) to call the app's persist_session() (or
otherwise trigger the same shutdown flow that leads to impl Drop for
PalimpsestApp) before exiting instead of calling std::process::exit directly.
Also remove direct calls to repo.fetch()/repo.pull()/repo.push() from
handle_quick_launch_action and move GitRepo::fetch/pull/push work off the egui
UI thread (spawn a background task/thread or use a worker queue) and marshal
progress/errors back to the UI so the render/update path is non-blocking and can
display progress or errors to the user.

In `@src/state/mod.rs`:
- Around line 560-561: The code currently calls clear_cache() unconditionally
after handling AppAction::ActivateTab and AppAction::CloseTab which evicts git
cache even when the active repository hasn't changed; update the handlers for
AppAction::ActivateTab and AppAction::CloseTab so they first determine whether
the active repo actually changes (e.g., capture the current active repo/id, call
activate_tab(index) or close_tab(index), then compare the new active repo/id)
and only call clear_cache() when the active repo changed (or when the closed tab
was the active tab). Use the existing functions activate_tab, close_tab and
clear_cache to implement this conditional cache clearing.
- Around line 140-143: AppAction::ActivateTab and AppAction::CloseTab currently
call .clear_cache() unconditionally causing
cached_commits/branches/remotes/tags, cached_status, last_refresh and repo_error
to be wiped even when current_repo doesn't change; update the handling so you
only call .clear_cache() when the active repository actually changes (e.g.,
compare the target repo id/path to state.current_repo before clearing), and for
CloseTab only clear when the closed tab was the active tab and the resulting
current_repo differs. Also remove or fix the session persistence comment about
std::fs::rename(&temp_path, &path) on Windows—don’t assume rename fails due to
existing destination; ensure any platform-specific rename handling is accurate
or add a comment referencing behavior rather than the incorrect assertion.

In `@src/ui/body.rs`:
- Around line 556-563: The early return when app_state.cached_commits.is_empty()
prevents commit_panel::show_cached from being called and thus hides
stage/discard controls; remove the return and instead render the logo as an
informational empty-state element while still allowing commit_panel::show_cached
to run (or call commit_panel::show_cached unconditionally after drawing the
logo). Locate the block using app_state.cached_commits, the
egui::Image/logo_rect creation, and the commit_panel::show_cached call and
change control flow so the logo is drawn but the function does not exit early,
ensuring commit_panel::show_cached executes even when cached_commits is empty.

In `@src/ui/repo_manager_sidebar.rs`:
- Around line 177-179: The persistent ID is derived from the basename via
repo_name(&repo.path) which causes collisions for different repos with the same
folder name; update the ui.make_persistent_id calls (the keys "manager_repo" and
"manager_repo_recent") to use a path-unique identifier instead of name—e.g., use
repo.path (or a canonicalized/absolute string like path.to_string_lossy() or a
hashed full path) when building the tuple key passed to ui.make_persistent_id so
each repo row gets a unique persistent ID tied to its full path.

In `@src/ui/repo_manager.rs`:
- Around line 51-64: is_github_url() and github_links() currently treat any
string containing "github.com" as valid and blindly append paths; update them to
properly validate and normalize GitHub remotes by: (1) parsing inputs as either
an HTTPS/HTTP URL (use Url parsing and ensure host == "github.com") or an SSH
remote pattern like "git@github.com:org/repo(.git)?" (match that regex), (2)
extract the "org/repo" path, trim a trailing ".git" and extra slashes, and build
a canonical base like "https://github.com/{org}/{repo}", and (3) return None for
non-matching hosts or malformed inputs; apply these changes in the functions
is_github_url and github_links so github_links uses the normalized base to
format "{base}/issues" and "{base}/pulls".

In `@src/ui/tabbar.rs`:
- Around line 173-197: The activation hit is checked before the close button, so
clicks on close_rect get consumed by ui.interact(rect, ...) and return
TabAction::Activate(index) before the close check; to fix, test the close button
interaction first (call ui.interact(close_rect,
ui.make_persistent_id(("tabbar_close", index)), egui::Sense::click()) and if
clicked return TabAction::Close(index)) or otherwise only then call
ui.interact(rect, ...) and return TabAction::Activate(index); use the existing
symbols close_rect, ui.interact(...), TabAction::Close, and TabAction::Activate
to locate and reorder the checks inside paint_tab.

In `@src/ui/toolbar.rs`:
- Around line 106-113: The stash dropdown currently uses a separate
toolbar_button (toolbar_button, ACTION_WIDTH, STACK, CARET_DOWN) and only
creates ui.menu_button(...) when stash_clicked is true, but egui's menu_button
must itself be clicked to open its popup; change the UI so the menu_button is
the actual interactive widget: replace the toolbar_button+conditional with a
single ui.menu_button(...) that renders the "Stash" label and caret (or mimic
ACTION_WIDTH/STACK/CARET_DOWN visuals) and inside its closure call
drop(ui.button("Stash changes")) etc.; alternately have toolbar_button call
ui.menu_button directly (i.e., return the menu_button response) so the popup is
attached to the clickable widget that the user actually clicks.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: eb2e64cc-d8ec-4ede-9c72-beb08f288837

📥 Commits

Reviewing files that changed from the base of the PR and between eee3419 and 2de53d4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • Cargo.toml
  • src/git/models.rs
  • src/git/repo.rs
  • src/main.rs
  • src/state/mod.rs
  • src/ui/body.rs
  • src/ui/command_palette.rs
  • src/ui/mod.rs
  • src/ui/repo_manager.rs
  • src/ui/repo_manager_body.rs
  • src/ui/repo_manager_sidebar.rs
  • src/ui/sidebar.rs
  • src/ui/tabbar.rs
  • src/ui/titlebar.rs
  • src/ui/toolbar.rs

Comment thread src/git/repo.rs
Comment on lines +130 to +135
let author = target.author();
Ok(Tag {
name: name.to_string(),
target_hash: target.id().to_string()[..7].to_string(),
author: author.name().unwrap_or("Unknown").to_string(),
timestamp: secs_to_system_time(author.when().seconds()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print the relevant section around the mentioned lines
sed -n '1,220p' src/git/repo.rs | nl -ba | sed -n '110,170p'

# Locate Tag struct definition and related fields/docs
rg -n "struct Tag\b|pub struct Tag\b|Tag \{" src/git/repo.rs
rg -n "author:|timestamp:" src/git/repo.rs
rg -n "peel_to_tag|peel_to_commit|revparse_single|find_reference|tagger\(" src/git/repo.rs

Repository: parazeeknova/palimpsest

Length of output: 110


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print relevant section with line numbers (no nl dependency)
sed -n '100,170p' src/git/repo.rs | cat -n

# Locate Tag struct definition
rg -n "pub struct Tag\b|struct Tag\b|Tag \{" src/git/repo.rs

# Search for author/timestamp usage and tag peeling/reference logic
rg -n "author:|timestamp:|secs_to_system_time|peel_to_tag|peel_to_commit|revparse_single|find_reference|tagger\(" src/git/repo.rs

Repository: parazeeknova/palimpsest

Length of output: 4100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect around where Tag fields are constructed/declared (per rg hits around ~425)
sed -n '380,470p' src/git/repo.rs | cat -n

# Find Tag struct definition (and any docs/notes)
rg -n "pub struct Tag\b|struct Tag\b" src/git/repo.rs
rg -n "author:.*Unknown|timestamp:.*secs_to_system_time" src/git/repo.rs

# Check if annotated-tag peeling is used anywhere else in the codebase
rg -n "peel_to_tag" .

# Check semantics elsewhere (where Tag.author / Tag.timestamp are used)
rg -n "\.author\b|\.timestamp\b|Tag \{|\bfn tags\b" src/git/repo.rs

Repository: parazeeknova/palimpsest

Length of output: 3838


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# File length (helps pick correct ranges)
wc -l src/git/repo.rs || true

# Find Tag struct definition anywhere in the repo
rg -n "pub struct Tag\b|struct Tag\b" .

# Find annotated-tag peeling usage anywhere
rg -n "peel_to_tag" .

# Inspect where Tag.author / Tag.timestamp are consumed (to infer intended meaning)
rg -n "\.author\b|\.timestamp\b" src | head -n 80

Repository: parazeeknova/palimpsest

Length of output: 127


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect Tag model definition (and docs/comments)
sed -n '1,120p' src/git/models.rs | cat -n

# See how Tag.author / Tag.timestamp are used elsewhere
rg -n "Tag\s*\{|\.author\b|\.timestamp\b" src | head -n 120

# Check if annotated-tag metadata is handled anywhere (peel_to_tag usage)
rg -n "peel_to_tag" . || true

Repository: parazeeknova/palimpsest

Length of output: 5707


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect tag model usage/mapping points
sed -n '90,160p' src/git/models.rs | cat -n

# Inspect cached/manager tag structs and how Tag fields are used
sed -n '180,330p' src/state/mod.rs | cat -n

# Inspect mapping where ManagerTag is built from git tags
rg -n "ManagerTag\s*\{|CachedTag\s*\{|tags:\s*vec!\[|map\(\|t\| CachedTag" src/state/mod.rs
sed -n '430,520p' src/state/mod.rs | cat -n

# Inspect UI rendering for tag author column
sed -n '520,660p' src/ui/repo_manager_body.rs | cat -n

# Look for any mention/tagger wording
rg -n "tagger|annotated|peel_to_tag|lightweight tag" src | head -n 80

Repository: parazeeknova/palimpsest

Length of output: 16768


Use annotated tagger metadata for Tag.author/timestamp when available.

src/git/repo.rs’s tags() currently peels each tag to a commit (oid.peel_to_commit()) and sets Tag.author/Tag.timestamp from target.author()/author.when(). For annotated tags, this ignores the tag object’s tagger/tag time, making the displayed tag metadata inaccurate.

Suggested approach
-                let oid = self.repo.revparse_single(&format!("refs/tags/{}", name))?;
-                let target = oid.peel_to_commit()?;
-                let author = target.author();
+                let tag_ref = self.repo.find_reference(&format!("refs/tags/{}", name))?;
+                let target = tag_ref.peel_to_commit()?;
+                let (author_name, timestamp) = if let Ok(tag_obj) = tag_ref.peel_to_tag() {
+                    if let Some(tagger) = tag_obj.tagger() {
+                        (
+                            tagger.name().unwrap_or("Unknown").to_string(),
+                            secs_to_system_time(tagger.when().seconds()),
+                        )
+                    } else {
+                        let a = target.author();
+                        (
+                            a.name().unwrap_or("Unknown").to_string(),
+                            secs_to_system_time(a.when().seconds()),
+                        )
+                    }
+                } else {
+                    let a = target.author();
+                    (
+                        a.name().unwrap_or("Unknown").to_string(),
+                        secs_to_system_time(a.when().seconds()),
+                    )
+                };
                 Ok(Tag {
                     name: name.to_string(),
                     target_hash: target.id().to_string()[..7].to_string(),
-                    author: author.name().unwrap_or("Unknown").to_string(),
-                    timestamp: secs_to_system_time(author.when().seconds()),
+                    author: author_name,
+                    timestamp,
                 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/git/repo.rs` around lines 130 - 135, The tags() logic peels tag OIDs to
commits and always uses target.author()/author.when(), which misses annotated
tag metadata; update tags() to detect annotated tag objects first (e.g., try to
peel/find the tag object for the OID or use Object::peel_to_tag /
repo.find_tag), and when a tag object exists use its tagger().name() and
tagger().when() for Tag.author and Tag.timestamp, falling back to
target.author() / author.when() for lightweight tags or when tagger is absent;
update the code paths constructing Tag (the code building name, target_hash,
author, timestamp) to prefer tag.tagger() metadata.

Comment thread src/git/repo.rs
Comment on lines +601 to +616
let fetch_head = self.repo.find_reference("FETCH_HEAD")?;
let fetch_commit = fetch_head.peel_to_commit()?;
let refname = format!("refs/heads/{}", branch_name);
let mut local_ref = self.repo.find_reference(&refname)?;
let local_commit = local_ref.peel_to_commit()?;
let ancestor = self.repo.merge_base(local_commit.id(), fetch_commit.id())?;
if ancestor == fetch_commit.id() {
return Ok(());
}
let tree = self.repo.find_commit(fetch_commit.id())?.tree()?;
let mut checkout_opts = git2::build::CheckoutBuilder::new();
checkout_opts.force();
self.repo
.checkout_tree(tree.as_object(), Some(&mut checkout_opts))?;
local_ref.set_target(fetch_commit.id(), "pull: Fast-forward")?;
Ok(())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show pull() implementation around the referenced lines
sed -n '540,680p' src/git/repo.rs | cat -n

# Extract surrounding function signature and any relevant context (may be slightly larger)
rg -n "fn pull\\b|pull\\(" -S src/git/repo.rs

# Show GitError definition (or error type used for returning Err)
rg -n "enum GitError|struct GitError|type GitError|GitError::" -S src/git/repo.rs src/git -g'*.rs'

# Search for other uses of FETCH_HEAD and CheckoutBuilder::force() in the repo
rg -n "FETCH_HEAD" -S src -g'*.rs'
rg -n "CheckoutBuilder::new\\(\\)|force\\(\\)" -S src -g'*.rs'

# Locate where branch_name comes from (search for branch_name in repo.rs)
rg -n "branch_name" src/git/repo.rs

Repository: parazeeknova/palimpsest

Length of output: 8592


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find head_branch implementation
rg -n "fn head_branch\\b|head_branch\\(" -S src/git/repo.rs
sed -n '520,640p' src/git/repo.rs | cat -n

# Show the full head_branch() function body area
rg -n "fn head_branch\\b" -n src/git/repo.rs
# Print around the matched line(s)
line=$(rg -n "fn head_branch\\b" -n src/git/repo.rs | head -n1 | cut -d: -f1)
start=$((line-30))
end=$((line+80))
sed -n "${start},${end}p" src/git/repo.rs | cat -n

# Inspect any config/setup code that influences what remote.fetch(&[]) fetches
rg -n "find_remote\\(|remote\\.fetch\\(|set_refspecs|refspec|upstream" -S src/git/repo.rs src/git -g'*.rs'

Repository: parazeeknova/palimpsest

Length of output: 12264


pull() can rewrite diverged local history (and may update the wrong branch/commit from FETCH_HEAD)

  • pull() fetches with remote.fetch(&[] as &[&str], ...) (no refspec tied to the current branch_name) and then uses FETCH_HEAD as the update target for refs/heads/{branch_name}, so the fetched commit may not correspond to that branch.
  • It only no-ops when merge_base(local, fetch) == fetch (remote behind). When histories diverge (merge_base is neither local_commit nor fetch_commit), it still force-checkouts fetch_commit and moves the local ref to it, discarding local commits.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/git/repo.rs` around lines 601 - 616, In pull(), current logic fetches
into FETCH_HEAD and unconditionally force-checkouts and moves refs which can
update the wrong branch or discard divergent history; change the fetch call to
fetch the specific remote branch ref (use a refspec like
format!("refs/heads/{branch_name}:refs/remotes/{remote_name}/{branch_name}") or
equivalent) and then resolve the fetched commit from that remote-tracking ref
instead of FETCH_HEAD (replace uses of fetch_head/fetch_commit with the resolved
remote ref), and before modifying local_ref ensure you only fast-forward when
merge_base(local_commit, remote_commit) == local_commit; if histories diverge
(merge_base is neither), do not force-checkout or set_target—return an error or
require an explicit merge/rebase flow instead; keep checkout_tree/set_target
only for the safe fast-forward path.

Comment thread src/main.rs
Comment on lines +262 to +285
QuickLaunchAction::Fetch => {
if let Some(repo) = &self.git_repo {
match repo.fetch() {
Ok(()) => self.refresh_git_data(),
Err(e) => tracing::error!(error = %e, "Fetch failed"),
}
}
}
QuickLaunchAction::Pull => {
if let Some(repo) = &self.git_repo {
match repo.pull() {
Ok(()) => self.refresh_git_data(),
Err(e) => tracing::error!(error = %e, "Pull failed"),
}
}
}
QuickLaunchAction::Push => {
if let Some(repo) = &self.git_repo {
match repo.push() {
Ok(()) => self.refresh_git_data(),
Err(e) => tracing::error!(error = %e, "Push failed"),
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Fetch/Pull/Push are executed on the UI thread.

These remote operations are synchronous here, so network/disk latency can stall rendering and make the app feel hung.

Comment thread src/main.rs
Comment on lines +540 to +543
if ctx.input_mut(|i| i.consume_shortcut(&exit_shortcut)) {
tracing::info!("Exiting app via shortcut");
std::process::exit(0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Rust, does std::process::exitrunDrop destructors before terminating the process?

💡 Result:

No, std::process::exit does not run Drop destructors [1][2][3]. When you call this function, the process terminates immediately, meaning any objects currently in scope—on the current thread or any other thread—will not have their destructors executed [1][2]. Because this behavior can lead to resources not being cleaned up properly, it is recommended to only use std::process::exit at a point in your program where you are certain that no destructors need to run [1][4]. For a clean shutdown, it is preferred to return an ExitCode or Result from your main function instead, which allows the normal cleanup process to occur as the stack unwinds [1][2][4]. If you must exit from a deeply nested function due to an unrecoverable error, panicking (panic!) is often a safer alternative, as it will trigger stack unwinding and execute destructors along the way [4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/main.rs"
echo "== File: $file =="
wc -l "$file" || true

echo
echo "== Lines 520-570 =="
nl -ba "$file" | sed -n '520,570p'

echo
echo "== Lines 220-320 =="
nl -ba "$file" | sed -n '220,320p'

echo
echo "== Find Drop impl in file =="
rg -n "impl\s+Drop\s+for|Drop\s+for" "$file" || true

echo
echo "== Show Drop impl block (if any) =="
# Heuristic: print around first match for Drop
first_drop_line="$(rg -n "impl\s+Drop\s+for|Drop\s+for" "$file" | head -n1 | cut -d: -f1 || true)"
if [[ -n "${first_drop_line:-}" ]]; then
  start=$((first_drop_line-40))
  end=$((first_drop_line+120))
  nl -ba "$file" | sed -n "${start},${end}p"
fi

Repository: parazeeknova/palimpsest

Length of output: 190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/main.rs"

echo "== File: $file =="
wc -l "$file" || true

echo
echo "== Lines 520-570 =="
python3 - <<'PY'
import itertools
path="src/main.rs"
start, end = 520, 570
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:4d}: {line.rstrip()}")
        if i > end:
            break
PY

echo
echo "== Lines 220-320 =="
python3 - <<'PY'
import itertools
path="src/main.rs"
start, end = 220, 320
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:4d}: {line.rstrip()}")
        if i > end:
            break
PY

echo
echo "== Find consume_shortcut exit shortcut block =="
rg -n "consume_shortcut|exit_shortcut|std::process::exit" "$file" || true

echo
echo "== Find Drop implementation(s) in this file =="
rg -n "impl\s+Drop\s+for|impl\s*<[^>]*>\s*Drop\s+for|Drop\s+for" "$file" || true

Repository: parazeeknova/palimpsest

Length of output: 8084


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/main.rs"

echo "== Drop impl area (around line 646) =="
python3 - <<'PY'
path="src/main.rs"
start, end = 620, 710
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:4d}: {line.rstrip()}")
        if i > end:
            break
PY

echo
echo "== Find where handle_quick_launch_action is called =="
rg -n "handle_quick_launch_action\s*\(" "$file" || true

echo
echo "== Look for refresh_git_data implementation =="
rg -n "fn\s+refresh_git_data" "$file" || true

echo
echo "== Find persist_session implementation/call sites =="
rg -n "fn\s+persist_session|persist_session\s*\(" "$file" || true

Repository: parazeeknova/palimpsest

Length of output: 4537


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/main.rs"

echo "== persist_session impl & call sites (lines ~60-170) =="
python3 - <<'PY'
path="src/main.rs"
start, end = 40, 190
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:4d}: {line.rstrip()}")
        if i > end:
            break
PY

echo
echo "== Where handle_quick_launch_action is invoked (around line ~480-535) =="
python3 - <<'PY'
path="src/main.rs"
start, end = 460, 540
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:4d}: {line.rstrip()}")
        if i > end:
            break
PY

echo
echo "== Which type is git_repo and where repo.fetch/pull/push are implemented =="
rg -n "git_repo" "$file"

# Find repo.fetch/pull/push call sites (already, but keep focused)
rg -n "\.fetch\(\)|\.pull\(\)|\.push\(\)" "$file"

# Locate GitRepo type definition
rg -n "struct\s+GitRepo|type\s+GitRepo|enum\s+GitRepo|impl\s+GitRepo" -S src || true

# Locate fetch/pull/push implementations across repo
rg -n "fn\s+fetch\(|fn\s+pull\(|fn\s+push\(" -S src || true

Repository: parazeeknova/palimpsest

Length of output: 11298


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo_file="src/git/repo.rs"

echo "== GitRepo fetch/pull/push implementations (around lines ~560-660) =="
python3 - <<'PY'
path="src/git/repo.rs"
start, end = 540, 690
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:4d}: {line.rstrip()}")
        if i > end:
            break
PY

echo
echo "== Any threading/async usage inside fetch/pull/push blocks (search for spawn/join/tokio/async) =="
rg -n "spawn|join|tokio::|async\s+fn|thread::" "$repo_file" || true

Repository: parazeeknova/palimpsest

Length of output: 4844


Fix Cmd/Ctrl+Q exit to persist session (and avoid blocking UI on git fetch/pull/push).

  • src/main.rs Cmd/Ctrl+Q shortcut exits via std::process::exit(0) (lines 540-543), which terminates immediately and bypasses impl Drop for PalimpsestApp (calls persist_session() at lines 646-649). Keyboard shortcut path should call self.persist_session() before exiting (quick-launch exit already does).

    Suggested fix
                 if ctx.input_mut(|i| i.consume_shortcut(&exit_shortcut)) {
                     tracing::info!("Exiting app via shortcut");
    +                self.persist_session();
                     std::process::exit(0);
                 }
  • handle_quick_launch_action calls repo.fetch()/pull()/push() directly on user actions (lines 262-285). GitRepo::fetch/pull/push in src/git/repo.rs run synchronous git2 operations (e.g., remote.fetch, checkout_tree, remote.push), so triggering these from the egui update/render path can stall frame updates; move git work off the UI thread (and show progress/error to the user).

📝 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.

Suggested change
if ctx.input_mut(|i| i.consume_shortcut(&exit_shortcut)) {
tracing::info!("Exiting app via shortcut");
std::process::exit(0);
}
if ctx.input_mut(|i| i.consume_shortcut(&exit_shortcut)) {
tracing::info!("Exiting app via shortcut");
self.persist_session();
std::process::exit(0);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.rs` around lines 540 - 543, The Cmd/Ctrl+Q shortcut currently calls
std::process::exit(0) and bypasses PalimpsestApp cleanup; change the shortcut
handling in main.rs (the ctx.input_mut(...) branch that checks
consume_shortcut(&exit_shortcut)) to call the app's persist_session() (or
otherwise trigger the same shutdown flow that leads to impl Drop for
PalimpsestApp) before exiting instead of calling std::process::exit directly.
Also remove direct calls to repo.fetch()/repo.pull()/repo.push() from
handle_quick_launch_action and move GitRepo::fetch/pull/push work off the egui
UI thread (spawn a background task/thread or use a worker queue) and marshal
progress/errors back to the UI so the render/update path is non-blocking and can
display progress or errors to the user.

Comment thread src/state/mod.rs
Comment on lines +140 to +143
if let Err(error) = fs::rename(&temp_path, &path) {
tracing::warn!(from = %temp_path.display(), to = %path.display(), error = %error, "Failed to commit session file");
let _ = fs::remove_file(&temp_path);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the session commit block around the referenced lines
echo "---- src/state/mod.rs: around session commit (140-170) ----"
sed -n '120,180p' src/state/mod.rs | nl -ba | sed -n '1,120p'

# Show the tab activation/closure handlers around the referenced lines (560-620)
echo "---- src/state/mod.rs: around tab actions cache clear (530-620) ----"
sed -n '520,640p' src/state/mod.rs | nl -ba | sed -n '1,180p'

# Search for rename/temp commit usage elsewhere in the file
echo "---- occurrences of fs::rename and temp_path ----"
rg -n "temp_path|fs::rename|remove_file\(&temp_path" src/state/mod.rs

# Search for cache clear on tab activation/closure
echo "---- occurrences of cache clear patterns ----"
rg -n "clear.*cache|cache.*clear|invalidate.*cache|cache\.clear" src/state/mod.rs

Repository: parazeeknova/palimpsest

Length of output: 188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/state/mod.rs"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"
  exit 1
fi

echo "---- src/state/mod.rs exists; size ----"
wc -l "$FILE"

echo "---- src/state/mod.rs: around session commit (140-170) ----"
sed -n '120,180p' "$FILE" | awk '{printf "%4d\t%s\n", NR+119, $0}' | sed -n '1,120p'

echo "---- src/state/mod.rs: around tab actions cache clear (530-620) ----"
sed -n '520,640p' "$FILE" | awk '{printf "%4d\t%s\n", NR+519, $0}' | sed -n '1,200p'

echo "---- occurrences of fs::rename and temp_path ----"
rg -n "temp_path|fs::rename|remove_file\(&temp_path" "$FILE" || true

echo "---- occurrences of cache clear patterns ----"
rg -n "clear.*cache|cache.*clear|invalidate.*cache|cache\.clear" "$FILE" || true

Repository: parazeeknova/palimpsest

Length of output: 10073


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/state/mod.rs"

echo "---- src/state/mod.rs: around clear_cache (480-540) ----"
sed -n '430,540p' "$FILE" | awk '{printf "%4d\t%s\n", NR+429, $0}' | sed -n '1,140p'

echo "---- src/state/mod.rs: around activate_tab / close_tab (540-610) ----"
sed -n '540,640p' "$FILE" | awk '{printf "%4d\t%s\n", NR+539, $0}' | sed -n '1,140p'

echo "---- src/state/mod.rs: clear_cache definition lines ----"
rg -n "fn clear_cache" "$FILE"

Repository: parazeeknova/palimpsest

Length of output: 9396


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/state/mod.rs"

echo "---- occurrences of clear_non_persistent_state ----"
rg -n "clear_non_persistent_state|fn activate_tab|fn close_tab" "$FILE"

echo "---- around clear_non_persistent_state ----"
# Print a window around the definition if found
LINE_CLEAR=$(rg -n "clear_non_persistent_state" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE_CLEAR:-}" ]; then
  START=$((LINE_CLEAR-80)); END=$((LINE_CLEAR+80))
  sed -n "${START},${END}p" "$FILE" | awk '{print NR+0 "\t" $0}' | head -n 220 || true
fi

echo "---- around activate_tab definition ----"
LINE_ACT=$(rg -n "fn activate_tab" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE_ACT:-}" ]; then
  START=$((LINE_ACT-80)); END=$((LINE_ACT+120))
  sed -n "${START},${END}p" "$FILE" | awk '{printf "%4d\t%s\n", NR+('"$START"'-1), $0}' | head -n 260
fi

echo "---- around close_tab definition ----"
LINE_CLOSE=$(rg -n "fn close_tab" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE_CLOSE:-}" ]; then
  START=$((LINE_CLOSE-80)); END=$((LINE_CLOSE+120))
  sed -n "${START},${END}p" "$FILE" | awk '{printf "%4d\t%s\n", NR+('"$START"'-1), $0}' | head -n 260
fi

Repository: parazeeknova/palimpsest

Length of output: 19302


🌐 Web query:

std::fs::rename windows destination exists behavior Rust documentation fs::rename fails if destination exists on Windows

💡 Result:

The behavior of std::fs::rename on Windows is designed to replace the destination file if it already exists, effectively behaving like a move-and-overwrite operation [1]. Key points regarding its behavior on Windows include: 1. Overwriting: By default, if the destination path (to) exists, std::fs::rename will replace it with the source file (from) [1]. 2. Underlying Mechanisms: Rust currently uses MoveFileExW or SetFileInformationByHandle on Windows [1]. On modern versions of Windows (Windows 10 1607 and later) where supported by the filesystem, it attempts to use FileRenameInfoEx with POSIX semantics, which provides behavior closer to Unix-like systems (e.g., allowing atomic replacement even if the destination is open, provided it is shared correctly) [2][3][1]. 3. Constraints: Historically, and on systems where POSIX-like renaming is not supported, the destination must not be a directory [1]. If the source is a file, the destination must also be a file [1]. 4. Platform Differences: The behavior when both source and destination exist can differ between platforms [1]. On Unix, if the source is a directory, the destination must be an empty directory [1]. Windows behavior has converged toward this in recent years, but historically allowed broader overwriting when using the underlying MoveFileEx APIs [1]. If you require an operation that fails when the destination already exists (a non-overwriting rename), the standard library does not currently provide a direct cross-platform primitive, though there have been community discussions regarding a potential std::fs::rename_noreplace function [4]. Users often must implement their own checks or use platform-specific APIs to ensure a target does not exist before renaming [4].

Citations:


Avoid unconditional cache eviction on tab activation/closure

  • AppAction::ActivateTab and AppAction::CloseTab always call .clear_cache(), which clears cached_commits/branches/remotes/tags, cached_status, last_refresh, and repo_error even when current_repo doesn’t change (e.g., re-activating the already-active tab or closing a non-active tab), causing visible data to vanish until refreshed.
  • The session persistence concern about std::fs::rename(&temp_path, &path) failing on Windows when path already exists doesn’t apply here: on Windows, rename overwrites the destination file.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/state/mod.rs` around lines 140 - 143, AppAction::ActivateTab and
AppAction::CloseTab currently call .clear_cache() unconditionally causing
cached_commits/branches/remotes/tags, cached_status, last_refresh and repo_error
to be wiped even when current_repo doesn't change; update the handling so you
only call .clear_cache() when the active repository actually changes (e.g.,
compare the target repo id/path to state.current_repo before clearing), and for
CloseTab only clear when the closed tab was the active tab and the resulting
current_repo differs. Also remove or fix the session persistence comment about
std::fs::rename(&temp_path, &path) on Windows—don’t assume rename fails due to
existing destination; ensure any platform-specific rename handling is accurate
or add a comment referencing behavior rather than the incorrect assertion.

Comment thread src/ui/body.rs
Comment on lines +556 to +563
if app_state.cached_commits.is_empty() {
let logo = egui::Image::new(egui::include_image!("../assets/logo.svg"))
.tint(egui::Color32::from_white_alpha(40))
.fit_to_exact_size(egui::vec2(200.0, 200.0));
let logo_rect = egui::Rect::from_center_size(rect.center(), egui::vec2(200.0, 200.0));
ui.put(logo_rect, logo);
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Empty-state early return blocks commit-panel actions.

When commit history is empty, this returns before commit_panel::show_cached, so users with uncommitted changes in a new repo lose stage/discard controls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/body.rs` around lines 556 - 563, The early return when
app_state.cached_commits.is_empty() prevents commit_panel::show_cached from
being called and thus hides stage/discard controls; remove the return and
instead render the logo as an informational empty-state element while still
allowing commit_panel::show_cached to run (or call commit_panel::show_cached
unconditionally after drawing the logo). Locate the block using
app_state.cached_commits, the egui::Image/logo_rect creation, and the
commit_panel::show_cached call and change control flow so the logo is drawn but
the function does not exit early, ensuring commit_panel::show_cached executes
even when cached_commits is empty.

Comment on lines +177 to +179
ui.make_persistent_id(("manager_repo", name)),
egui::Sense::click(),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/ui/repo_manager_sidebar.rs"

# Show the relevant sections around the reported lines.
sed -n '130,260p' "$FILE" | nl -ba | sed -n '1,160p'

Repository: parazeeknova/palimpsest

Length of output: 110


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/ui/repo_manager_sidebar.rs"

# Find definitions/usages of the persistent id keys.
rg -n "make_persistent_id\\(\\(\"manager_repo(_recent)?\",\\s*([^)]+)\\)\\)" "$FILE" || true
rg -n "fn paint_repo_row|fn paint_repo_row_with_time|paint_repo_row(_with_time)\\(" "$FILE"

Repository: parazeeknova/palimpsest

Length of output: 303


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/ui/repo_manager_sidebar.rs"

# Print the function signatures and bodies (bounded).
rg -n "fn paint_repo_row|fn paint_repo_row_with_time" "$FILE"

Repository: parazeeknova/palimpsest

Length of output: 123


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/ui/repo_manager_sidebar.rs"

# Print with line numbers using awk (nl not available)
awk 'NR>=150 && NR<=245 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: parazeeknova/palimpsest

Length of output: 2772


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/ui/repo_manager_sidebar.rs"

# Also capture the repo row call sites with enough context
awk 'NR>=190 && NR<=235 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: parazeeknova/palimpsest

Length of output: 1350


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/ui/repo_manager_sidebar.rs"

# Show earlier context where repo.path / name are computed (best-effort; adjust window if needed)
awk 'NR>=40 && NR<=120 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: parazeeknova/palimpsest

Length of output: 2707


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate repo_name definition and any helpers it uses
rg -n "fn repo_name\\b" -S .
rg -n "repo_name\\(" -S src/ui/repo_manager_sidebar.rs

Repository: parazeeknova/palimpsest

Length of output: 475


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/ui/repo_manager_sidebar.rs"
awk 'NR>=220 && NR<=310 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: parazeeknova/palimpsest

Length of output: 1175


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/ui/repo_manager_sidebar.rs"
rg -n "paint_repo_row(_with_time)?\\(" "$FILE"

Repository: parazeeknova/palimpsest

Length of output: 278


Use path-based interaction IDs for repo rows.

ui.make_persistent_id is keyed by name, but name comes from repo_name(&repo.path) which uses Path::new(path).file_name() (basename). Two different repo.path values with the same folder name will therefore share the same egui persistent ID (("manager_repo", name) / ("manager_repo_recent", name)), risking incorrect click/hover state.

💡 Suggested fix
-fn paint_repo_row(
+fn paint_repo_row(
     ui: &egui::Ui,
     rect: egui::Rect,
     y: f32,
+    repo_id: &str,
     name: &str,
@@
     let response = ui.interact(
         row,
-        ui.make_persistent_id(("manager_repo", name)),
+        ui.make_persistent_id(("manager_repo", repo_id)),
         egui::Sense::click(),
     );
-fn paint_repo_row_with_time(
+fn paint_repo_row_with_time(
     ui: &egui::Ui,
     rect: egui::Rect,
     y: f32,
+    repo_id: &str,
     name: &str,
@@
     let response = ui.interact(
         row,
-        ui.make_persistent_id(("manager_repo_recent", name)),
+        ui.make_persistent_id(("manager_repo_recent", repo_id)),
         egui::Sense::click(),
     );
-            let clicked = paint_repo_row_with_time(
+            let clicked = paint_repo_row_with_time(
                 ui,
                 rect,
                 y,
+                &repo.path,
                 name,
                 &time_ago,
@@
-            let clicked = paint_repo_row(ui, rect, y, name, is_selected, text, selected, muted);
+            let clicked = paint_repo_row(
+                ui,
+                rect,
+                y,
+                &repo.path,
+                name,
+                is_selected,
+                text,
+                selected,
+                muted,
+            );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/repo_manager_sidebar.rs` around lines 177 - 179, The persistent ID is
derived from the basename via repo_name(&repo.path) which causes collisions for
different repos with the same folder name; update the ui.make_persistent_id
calls (the keys "manager_repo" and "manager_repo_recent") to use a path-unique
identifier instead of name—e.g., use repo.path (or a canonicalized/absolute
string like path.to_string_lossy() or a hashed full path) when building the
tuple key passed to ui.make_persistent_id so each repo row gets a unique
persistent ID tied to its full path.

Comment thread src/ui/repo_manager.rs
Comment on lines +51 to +64
pub fn is_github_url(url: &str) -> bool {
url.contains("github.com")
}

pub fn github_links(url: &str) -> Option<(String, String, String)> {
if !is_github_url(url) {
return None;
}
let base = url.trim_end_matches(".git");
Some((
base.to_string(),
format!("{}/issues", base),
format!("{}/pulls", base),
))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize and validate GitHub remotes before generating web links.

Current logic accepts any string containing github.com and appends /issues//pulls directly. This breaks common SSH remotes (git@github.com:org/repo.git) and can misclassify non-GitHub hosts.

💡 Suggested fix
-pub fn is_github_url(url: &str) -> bool {
-    url.contains("github.com")
-}
+pub fn is_github_url(url: &str) -> bool {
+    github_base(url).is_some()
+}

 pub fn github_links(url: &str) -> Option<(String, String, String)> {
-    if !is_github_url(url) {
-        return None;
-    }
-    let base = url.trim_end_matches(".git");
+    let base = github_base(url)?;
     Some((
-        base.to_string(),
+        base.clone(),
         format!("{}/issues", base),
         format!("{}/pulls", base),
     ))
 }
+
+fn github_base(url: &str) -> Option<String> {
+    let trimmed = url.trim_end_matches(".git").trim();
+    if let Some(path) = trimmed.strip_prefix("git@github.com:") {
+        return Some(format!("https://github.com/{}", path));
+    }
+    if let Some(path) = trimmed.strip_prefix("https://github.com/") {
+        return Some(format!("https://github.com/{}", path));
+    }
+    if let Some(path) = trimmed.strip_prefix("http://github.com/") {
+        return Some(format!("https://github.com/{}", path));
+    }
+    None
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/repo_manager.rs` around lines 51 - 64, is_github_url() and
github_links() currently treat any string containing "github.com" as valid and
blindly append paths; update them to properly validate and normalize GitHub
remotes by: (1) parsing inputs as either an HTTPS/HTTP URL (use Url parsing and
ensure host == "github.com") or an SSH remote pattern like
"git@github.com:org/repo(.git)?" (match that regex), (2) extract the "org/repo"
path, trim a trailing ".git" and extra slashes, and build a canonical base like
"https://github.com/{org}/{repo}", and (3) return None for non-matching hosts or
malformed inputs; apply these changes in the functions is_github_url and
github_links so github_links uses the normalized base to format "{base}/issues"
and "{base}/pulls".

Comment thread src/ui/tabbar.rs
Comment on lines +173 to +197
let activate_response = ui.interact(
rect,
ui.make_persistent_id(("tabbar_tab", index)),
egui::Sense::click(),
);
if activate_response.clicked() {
return Some(TabAction::Activate(index));
}

if tab.closeable {
ui.painter().text(
close_rect.center(),
egui::Align2::CENTER_CENTER,
X,
egui::FontId::proportional(10.0),
ui.visuals().text_color(),
);
let close_response = ui.interact(
close_rect,
ui.make_persistent_id(("tabbar_close", index)),
egui::Sense::click(),
);
if close_response.clicked() {
return Some(TabAction::Close(index));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant portion of src/ui/tabbar.rs
sed -n '130,230p' src/ui/tabbar.rs | cat -n

# Also locate where rect and close_rect are defined earlier in the file
rg -n "close_rect|let rect|Rect::from" src/ui/tabbar.rs | head -n 50

# Print surrounding context for close_rect/rect computation
# Extract a bit more around the first occurrence of close_rect
line=$(rg -n "close_rect" src/ui/tabbar.rs | head -n 1 | cut -d: -f1)
start=$((line-80))
end=$((line+80))
sed -n "${start},${end}p" src/ui/tabbar.rs | cat -n

Repository: parazeeknova/palimpsest

Length of output: 9952


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '130,230p' src/ui/tabbar.rs | cat -n
rg -n "close_rect|let rect|Rect::from" src/ui/tabbar.rs | head -n 50
line=$(rg -n "close_rect" src/ui/tabbar.rs | head -n 1 | cut -d: -f1)
start=$((line-80))
end=$((line+80))
sed -n "${start},${end}p" src/ui/tabbar.rs | cat -n

Repository: parazeeknova/palimpsest

Length of output: 9952


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '150,215p' src/ui/tabbar.rs | cat -n
rg -n "close_rect|rect\s*=|Rect::from_min_max|Rect::from_min_size" src/ui/tabbar.rs
echo "---- close_rect computation context ----"
line=$(rg -n "close_rect" src/ui/tabbar.rs | head -n 1 | cut -d: -f1)
start=$((line-120))
end=$((line+120))
sed -n "${start},${end}p" src/ui/tabbar.rs | cat -n

Repository: parazeeknova/palimpsest

Length of output: 11706


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find constants in tabbar.rs
rg -n "CLOSE_WIDTH|TABBAR_HEIGHT|PLUS_WIDTH|TAB_MAX_WIDTH|TAB_MIN_WIDTH" src/ui/tabbar.rs
# Show top of file for context around constants
sed -n '1,140p' src/ui/tabbar.rs | cat -n

Repository: parazeeknova/palimpsest

Length of output: 5553


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "CLOSE_WIDTH|TABBAR_HEIGHT|PLUS_WIDTH|TAB_MAX_WIDTH|TAB_MIN_WIDTH" src/ui/tabbar.rs
sed -n '1,140p' src/ui/tabbar.rs | cat -n

Repository: parazeeknova/palimpsest

Length of output: 5553


Fix tab activation consuming close clicks (src/ui/tabbar.rs)

paint_tab defines close_rect inside the tab rect (x: rect.right() - CLOSE_WIDTH - 4.0 .. rect.right() - 4.0, y: rect.center().y - 8.0 .. +8.0). Because the code calls ui.interact(rect, ...) and returns TabAction::Activate(index) before running ui.interact(close_rect, ...), clicks on the close hotspot will be consumed as activation.

💡 Suggested fix
-    let activate_response = ui.interact(
-        rect,
-        ui.make_persistent_id(("tabbar_tab", index)),
-        egui::Sense::click(),
-    );
-    if activate_response.clicked() {
-        return Some(TabAction::Activate(index));
-    }
-
     if tab.closeable {
         ui.painter().text(
             close_rect.center(),
             egui::Align2::CENTER_CENTER,
             X,
@@
         if close_response.clicked() {
             return Some(TabAction::Close(index));
         }
     }
+
+    let activate_rect = egui::Rect::from_min_max(
+        rect.left_top(),
+        egui::pos2(close_rect.left() - 2.0, rect.bottom()),
+    );
+    let activate_response = ui.interact(
+        activate_rect,
+        ui.make_persistent_id(("tabbar_tab", index)),
+        egui::Sense::click(),
+    );
+    if activate_response.clicked() {
+        return Some(TabAction::Activate(index));
+    }
📝 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.

Suggested change
let activate_response = ui.interact(
rect,
ui.make_persistent_id(("tabbar_tab", index)),
egui::Sense::click(),
);
if activate_response.clicked() {
return Some(TabAction::Activate(index));
}
if tab.closeable {
ui.painter().text(
close_rect.center(),
egui::Align2::CENTER_CENTER,
X,
egui::FontId::proportional(10.0),
ui.visuals().text_color(),
);
let close_response = ui.interact(
close_rect,
ui.make_persistent_id(("tabbar_close", index)),
egui::Sense::click(),
);
if close_response.clicked() {
return Some(TabAction::Close(index));
}
if tab.closeable {
ui.painter().text(
close_rect.center(),
egui::Align2::CENTER_CENTER,
X,
egui::FontId::proportional(10.0),
ui.visuals().text_color(),
);
let close_response = ui.interact(
close_rect,
ui.make_persistent_id(("tabbar_close", index)),
egui::Sense::click(),
);
if close_response.clicked() {
return Some(TabAction::Close(index));
}
}
let activate_rect = egui::Rect::from_min_max(
rect.left_top(),
egui::pos2(close_rect.left() - 2.0, rect.bottom()),
);
let activate_response = ui.interact(
activate_rect,
ui.make_persistent_id(("tabbar_tab", index)),
egui::Sense::click(),
);
if activate_response.clicked() {
return Some(TabAction::Activate(index));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/tabbar.rs` around lines 173 - 197, The activation hit is checked
before the close button, so clicks on close_rect get consumed by
ui.interact(rect, ...) and return TabAction::Activate(index) before the close
check; to fix, test the close button interaction first (call
ui.interact(close_rect, ui.make_persistent_id(("tabbar_close", index)),
egui::Sense::click()) and if clicked return TabAction::Close(index)) or
otherwise only then call ui.interact(rect, ...) and return
TabAction::Activate(index); use the existing symbols close_rect,
ui.interact(...), TabAction::Close, and TabAction::Activate to locate and
reorder the checks inside paint_tab.

Comment thread src/ui/toolbar.rs
Comment on lines +106 to +113
let stash_clicked = toolbar_button(ui, ACTION_WIDTH, STACK, "Stash", Some(CARET_DOWN));
if stash_clicked {
ui.menu_button(egui::RichText::new("").size(1.0), |ui| {
drop(ui.button("Stash changes"));
drop(ui.button("Apply stash"));
drop(ui.button("Pop stash"));
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In egui 0.34, does ui.menu_button(...)open from a click on a different widget, or only when themenu_button itself is clicked?

💡 Result:

In egui 0.34, ui.menu_button(...) opens its menu only when the menu_button’s own button is clicked (it is a thin wrapper around a Button that shows a Popup menu “when clicked”). It does not automatically open when some other, different widget is clicked. This follows from the API description: MenuButton “A thin wrapper around a Button that shows a Popup::menu when clicked” [1], and from the menu.rs implementation of menu_button, which is implemented as “Responds to primary clicks” and constructs the menu via stationary_menu_impl (i.e., the menu is tied to the button interaction state, not to other widgets) [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the referenced area in src/ui/toolbar.rs
sed -n '70,150p' src/ui/toolbar.rs | nl -ba | sed -n '70,150p' || true

# Search for stash-related variables/handlers in this file
rg -n "stash_clicked|Stash|Apply stash|Pop stash|menu_button" src/ui/toolbar.rs

Repository: parazeeknova/palimpsest

Length of output: 462


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show more context around the stash menu code
sed -n '1,220p' src/ui/toolbar.rs | sed -n '1,220p' | cat

# Locate toolbar_button definition and inspect it
rg -n "fn\s+toolbar_button|toolbar_button\(" src/ui/toolbar.rs
rg -n "menu_button\(" src/ui/toolbar.rs

Repository: parazeeknova/palimpsest

Length of output: 8850


Stash dropdown doesn’t open from clicking “Stash” (separate, conditional menu_button)

ui.menu_button(...) is only created when stash_clicked is true, but in egui the menu_button popup opens only when that menu_button widget itself is clicked—so the click on the “Stash” toolbar button won’t reliably trigger the dropdown (and the menu_button label is essentially empty via RichText::new("").size(1.0)).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/toolbar.rs` around lines 106 - 113, The stash dropdown currently uses
a separate toolbar_button (toolbar_button, ACTION_WIDTH, STACK, CARET_DOWN) and
only creates ui.menu_button(...) when stash_clicked is true, but egui's
menu_button must itself be clicked to open its popup; change the UI so the
menu_button is the actual interactive widget: replace the
toolbar_button+conditional with a single ui.menu_button(...) that renders the
"Stash" label and caret (or mimic ACTION_WIDTH/STACK/CARET_DOWN visuals) and
inside its closure call drop(ui.button("Stash changes")) etc.; alternately have
toolbar_button call ui.menu_button directly (i.e., return the menu_button
response) so the popup is attached to the clickable widget that the user
actually clicks.

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.

2 participants