Skip to content

feat(history): add native GTK Ctrl+H sidebar#304

Merged
bnema merged 15 commits into
mainfrom
feat/gtk-history-sidebar
Jun 13, 2026
Merged

feat(history): add native GTK Ctrl+H sidebar#304
bnema merged 15 commits into
mainfrom
feat/gtk-history-sidebar

Conversation

@bnema

@bnema bnema commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • replace Ctrl+H history with a native GTK history sidebar
  • add grouped-by-day browsing, FTS search, paging, keyboard-first interactions, theme integration, and config-backed sidebar width
  • keep navigation scoped to the owning browser window and refactor the sidebar into focused files with an explicit display-row model and narrow application port

Behavior

  • Ctrl+H toggles the native history sidebar
  • if the native sidebar is unavailable, Ctrl+H returns a clean error instead of falling back to dumb://history
  • default history activation keeps the sidebar open
  • Shift+Enter opens the selected history item in a new split

Validation

  • go test ./internal/...
  • go vet ./internal/...
  • focused Go/architecture review passes
  • wrap-up review passes with no remaining findings

Summary by CodeRabbit

  • New Features

    • Native GTK history sidebar: toggle with Ctrl+H, search, browse, delete, open-in-new-pane, and preserved scroll/selection.
    • Configurable sidebar width with sensible defaults and clamping.
    • Full keyboard navigation and activation behaviors.
  • Documentation

    • Keybindings updated to reflect Ctrl+H toggling the native GTK history sidebar and potential shortcut conflicts.
  • Tests

    • Extensive unit and UI tests added for sidebar behavior, rendering, search, keyboard handling, and config.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@bnema, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 2 hours, 20 minutes, and 43 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: aae63055-c214-4a7e-af6a-4a970b30726d

📥 Commits

Reviewing files that changed from the base of the PR and between 1ee4e97 and 217c6f0.

📒 Files selected for processing (2)
  • internal/ui/browser_window_history_sidebar.go
  • internal/ui/component/history_sidebar_rendering.go
📝 Walkthrough

Walkthrough

Adds a native GTK history sidebar: new history port, config (sidebar width), main-window sidebar pane and APIs, complete GTK sidebar component (widgets, keyboard, loading/search, rendering), dispatcher wiring for Ctrl+H, CSS, docs update, and extensive unit/integration tests.

Changes

Native GTK History Sidebar

Layer / File(s) Summary
Configuration & port contracts
internal/infrastructure/config/*, internal/application/port/history_sidebar.go, .mockery.yaml
Sidebar width defaults/schema/validation added; new HistorySidebarHistory port interface and mockery entry; compile-time conformance added to search usecase.
Main window & sidebar API
internal/ui/window/*, internal/ui/app.go, internal/ui/browser_window.go
MainWindow layout refactored to include sidebarBox and mainContentBox; sidebar width/visibility APIs (SetSidebarWidth, SetSidebarWidget, visibility queries); browser-window lifecycle wired to initialize/destroy sidebar and apply width on config reload.
History UI model
internal/ui/component/history_model.go, internal/ui/component/history_model_test.go
Day-based grouping, readable URL and relative-time helpers, explicit display row model, keyboard navigation primitives and unit tests covering edge cases.
Sidebar component core & widgets
internal/ui/component/history_sidebar.go, internal/ui/component/history_sidebar_widgets.go
HistorySidebar type, constructor, lifecycle (Show/Hide/Reload/Destroy), widget creation for search and list, and widget-exposure helpers.
Keyboard, activation, delete navigation
internal/ui/component/history_sidebar_keyboard.go
Capture-phase key controller handling Enter (modifiers), Escape, Delete (async delete & in-memory update), paging and group jumps, selection visibility helpers, activation gating.
Loading, search, generation guards
internal/ui/component/history_sidebar_loading_search.go, internal/ui/component/history_sidebar_search_test.go
Background paging with generation-safe fetches, debounced search + FTS, LoadMore via scroll, scroll/selection preservation, applySearchResults semantics and tests for stale/result handling.
Rendering & UI behaviors
internal/ui/component/history_sidebar_rendering.go
List rebuild, header/entry row rendering, empty/loading states, ensure-selection logic, defensive widget creation.
Dispatcher, keyboard binding & docs
internal/ui/dispatcher/keyboard.go, internal/ui/dispatcher/keyboard_test.go, docs/reference/keybindings.md
KeyboardDispatcher gets SetOnToggleHistorySidebar; ActionToggleHistorySystemView now calls the sidebar handler or returns a controlled error; docs updated to describe native sidebar behavior for Ctrl+H; tests adjusted.
Theme CSS & tests
internal/ui/theme/history_sidebar_css.go, internal/ui/theme/history_sidebar_css_test.go, internal/ui/theme/css.go
Generated GTK4 CSS for sidebar styling added and integrated into full theme; tests for selector coverage, accent variable usage, determinism, and palette switching.
Integration & wide tests
internal/ui/browser_window_history_sidebar.go, internal/ui/browser_window_history_sidebar_test.go, internal/ui/browser_window_test.go
Browser-window wiring for init/mount/toggle/show/hide, navigation callbacks (navigate/keep-open/new-pane/close), ownership protection across windows; extensive integration tests and nil-safety checks.
Test helpers & harnesses
internal/ui/component/history_test_helpers_test.go, other test files
Added test-only helpers and multiple unit/integration tests for model, sidebar search/browse behavior, generation guards, and UI-safe semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • bnema/dumber#235: Related changes to the Ctrl+H / history toggle keyboard handling path in the dispatcher.
  • bnema/dumber#193: Prior work on SearchHistoryUseCase conformance and related interface adjustments.

Poem

🐰 A little rabbit hops and spies,

A history sidebar before your eyes,
Grouped by days, you search and roam,
Ctrl+H brings the native home,
Hop in — your past is safe to comb.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.23% 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 directly and precisely describes the main feature being added: a native GTK sidebar for Ctrl+H history.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gtk-history-sidebar

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@internal/ui/component/history_sidebar_loading_search.go`:
- Around line 37-52: The code uses glib.IdleAdd directly inside the callback,
bypassing the component's injected idle scheduler; replace those direct
glib.IdleAdd calls with the component's scheduling wrapper hs.scheduleIdle(...)
so GTK-thread callbacks are dispatched via the same seam used elsewhere.
Concretely, where you create cb := glib.SourceFunc(...) and then call
glib.IdleAdd(&cb, 0), change that final call to hs.scheduleIdle(&cb) (and do the
same for the other occurrence that mirrors this pattern) so the scheduling goes
through hs.scheduleIdle and preserves existing behavior and testability.

In `@internal/ui/component/history_sidebar_widgets.go`:
- Around line 76-80: The comment is inaccurate because
hs.listBox.SetActivateOnSingleClick(true) enables single-click activation;
update the comment above rowActivatedCb (or the surrounding line) to accurately
describe activation behavior (e.g., "Connect row activation (single-click and
keyboard activation)" or "Connect row activation (single-click, Enter or
double-click as configured)") so it reflects that SetActivateOnSingleClick(true)
enables click-based activation; alternatively, if the intent was to require
Enter/double-click, change hs.listBox.SetActivateOnSingleClick(true) to false
and keep the original comment—ensure the comment and the
SetActivateOnSingleClick setting are consistent.

In `@internal/ui/component/history_sidebar.go`:
- Around line 235-273: The Show method holds hs.mu while calling hs.scheduleIdle
which can re-enter and deadlock; fix HistorySidebar.Show by collecting the
minimal data needed for each idle callback (e.g., capture hs.destroyed and
hs.searchEntry into locals or capture hs pointer but avoid holding hs.mu), then
release hs.mu before calling hs.scheduleIdle for both reloadCb and the focus cb.
Concretely: inside HistorySidebar.Show, lock and read required fields into local
variables, unlock (defer removed), then build the glib.SourceFunc closures (they
may still check hs.destroyed inside) and call hs.scheduleIdle; ensure the
visible flag and outerBox.SetVisible(true) remain set while holding the lock but
move the scheduleIdle invocations to after unlocking to avoid scheduling while
hs.mu is held.
🪄 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: 657c121d-e5bb-4bdb-8576-751b37566cda

📥 Commits

Reviewing files that changed from the base of the PR and between ac3d540 and 933fcaf.

📒 Files selected for processing (31)
  • .mockery.yaml
  • docs/reference/keybindings.md
  • internal/application/port/history_sidebar.go
  • internal/application/usecase/search_history.go
  • internal/infrastructure/config/defaults.go
  • internal/infrastructure/config/loader.go
  • internal/infrastructure/config/migrate.go
  • internal/infrastructure/config/schema.go
  • internal/infrastructure/config/schema_provider.go
  • internal/infrastructure/config/validation.go
  • internal/ui/app.go
  • internal/ui/browser_window.go
  • internal/ui/browser_window_history_sidebar.go
  • internal/ui/browser_window_history_sidebar_test.go
  • internal/ui/browser_window_test.go
  • internal/ui/component/history_model.go
  • internal/ui/component/history_model_test.go
  • internal/ui/component/history_sidebar.go
  • internal/ui/component/history_sidebar_keyboard.go
  • internal/ui/component/history_sidebar_loading_search.go
  • internal/ui/component/history_sidebar_rendering.go
  • internal/ui/component/history_sidebar_search_test.go
  • internal/ui/component/history_sidebar_widgets.go
  • internal/ui/component/history_test_helpers_test.go
  • internal/ui/dispatcher/keyboard.go
  • internal/ui/dispatcher/keyboard_test.go
  • internal/ui/theme/css.go
  • internal/ui/theme/history_sidebar_css.go
  • internal/ui/theme/history_sidebar_css_test.go
  • internal/ui/window/main_window.go
  • internal/ui/window/main_window_sidebar.go

Comment thread internal/ui/component/history_sidebar_loading_search.go
Comment thread internal/ui/component/history_sidebar_widgets.go
Comment thread internal/ui/component/history_sidebar.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/ui/browser_window_history_sidebar.go (1)

30-38: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Force the sidebar hidden during initialization.

Line 31 only updates the mirror flag. This path never calls HistorySidebar.Hide() or MainWindow.SetSidebarVisible(false), so the “hidden by default” behavior depends on external widget defaults instead of being enforced here.

Proposed fix
 	bw.historySidebar = sidebar
-	bw.sidebarVisible = false

 	// Mount into the main window's sidebar box
 	bw.mainWindow.SetSidebarWidget(sidebar.Widget())
+	bw.historySidebar.Hide()
+	bw.mainWindow.SetSidebarVisible(false)
+	bw.sidebarVisible = false
🤖 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 `@internal/ui/browser_window_history_sidebar.go` around lines 30 - 38, The code
only sets bw.sidebarVisible = false but doesn't actually hide the widget; update
the initialization in the constructor that sets bw.historySidebar and
bw.mainWindow to explicitly hide the sidebar by calling bw.historySidebar.Hide()
and bw.mainWindow.SetSidebarVisible(false) (call these after
bw.mainWindow.SetSidebarWidget(sidebar.Widget()) and before
bw.applySidebarWidthConfig(a)) so the "hidden by default" state is enforced
rather than relying on widget defaults.
🤖 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 `@internal/ui/component/history_sidebar_rendering.go`:
- Line 81: The empty-state label uses fmt.Sprintf with the %q verb which
produces Go-escaped literals (e.g. showing backslashes); update the
label.SetText calls that use fmt.Sprintf("No results for %q", query) to use a
plain-string format instead, e.g. fmt.Sprintf("No results for %s", query) (and
make the same change for the other occurrence in the file) so the query is shown
as normal text rather than a Go-escaped literal.

---

Outside diff comments:
In `@internal/ui/browser_window_history_sidebar.go`:
- Around line 30-38: The code only sets bw.sidebarVisible = false but doesn't
actually hide the widget; update the initialization in the constructor that sets
bw.historySidebar and bw.mainWindow to explicitly hide the sidebar by calling
bw.historySidebar.Hide() and bw.mainWindow.SetSidebarVisible(false) (call these
after bw.mainWindow.SetSidebarWidget(sidebar.Widget()) and before
bw.applySidebarWidthConfig(a)) so the "hidden by default" state is enforced
rather than relying on widget defaults.
🪄 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: 0a7c05b4-12c6-49a1-b387-2607e06f7df8

📥 Commits

Reviewing files that changed from the base of the PR and between 933fcaf and 1ee4e97.

📒 Files selected for processing (11)
  • internal/ui/browser_window_history_sidebar.go
  • internal/ui/browser_window_history_sidebar_test.go
  • internal/ui/component/history_model.go
  • internal/ui/component/history_model_test.go
  • internal/ui/component/history_sidebar.go
  • internal/ui/component/history_sidebar_keyboard.go
  • internal/ui/component/history_sidebar_loading_search.go
  • internal/ui/component/history_sidebar_rendering.go
  • internal/ui/component/history_sidebar_search_test.go
  • internal/ui/component/history_sidebar_widgets.go
  • internal/ui/window/main_window.go

Comment thread internal/ui/component/history_sidebar_rendering.go Outdated
@bnema bnema merged commit cbe806a into main Jun 13, 2026
5 checks passed
@bnema bnema deleted the feat/gtk-history-sidebar branch June 13, 2026 09:45
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