Skip to content

feat(history): unified history & recommendations window with embedded settings#12

Merged
rmarinsky merged 4 commits into
mainfrom
feat/history-recommendations-window
May 19, 2026
Merged

feat(history): unified history & recommendations window with embedded settings#12
rmarinsky merged 4 commits into
mainfrom
feat/history-recommendations-window

Conversation

@rmarinsky

@rmarinsky rmarinsky commented May 19, 2026

Copy link
Copy Markdown
Owner

Closes #11.

Summary

Adds a dedicated history & recommendations window that opens on Spotlight launch (or any app reopen), with a NavigationSplitView sidebar grouping data sources (clipboard, replacements, recommendations) alongside the previously-standalone settings (General / Hotkeys / AutoFix / Analytics / About). The standalone SwiftUI Settings scene is removed — both menu-bar entries now open this window with a pre-selected section.

Side-effect that fixes #11

Before this change, the only way to reach Settings was through the menu-bar icon, so unchecking «Показувати іконку в меню-барі» trapped the user with no UI. The new window opens unconditionally on Spotlight launch (and on every app reopen via applicationShouldHandleReopen), so the user can recover by typing "Papuga" in Spotlight → window appears → sidebar → "Загальні" → re-enable the icon toggle.

Rest of the feature

  • Replacement history: new ReplacementHistoryStore persists every manual switch + AutoFix apply + AutoFix undo to JSONL in Application Support, with retention presets (1 week / 1 month / 3 months / forever). Stored text is truncated to 80 chars.
  • Fibonacci-based recommendations: RecommendationEngine analyses the history at thresholds 3 → 5 → 8 → 13 → 21 and surfaces three kinds of suggestions:
    • add a word to autoFixAllowlist (frequently undone autofix);
    • create a CustomAutoReplaceRule source→target (frequently performed manual switch);
    • add an app to autoFixBlocklist (many undos in one bundle).
  • Personal rules actually fire: AutoFixController consults customAutoReplaceRules before the language scorer, so accepted recommendations replace during typing.
  • Settings folded into the window: GeneralTab gains a "Історія замін" block (toggle / retention / clear / open-on-launch); AutoFixTab gains a "Власні правила автозаміни" editor.

Test plan

  • Issue Неможливо повернути іконку Papuga в «Смуга меню» #11 recovery: hide the menu-bar icon → ⌘+Space → "Papuga" → window opens → "Загальні" → re-enable icon → menu-bar icon returns.
  • Spotlight launch opens the history window with sidebar "Дані / Налаштування".
  • Menu bar "Історія та рекомендації…" → clipboard section; "Налаштування…" → General section.
  • Perform several manual switches → entries appear in "Заміни".
  • Trigger AutoFix and let it stand → .autoFixApplied event in history.
  • Undo AutoFix within the grace window → .autoFixUndone event.
  • Repeat same undo 3× → tier-1 card in "Рекомендації"; 8× → tier-3.
  • Accept "Додати у allowlist" → word lands in autoFixAllowlist; next typing of it is not auto-corrected.
  • Accept "Створити правило" for a manual pair → typing the source word + space auto-replaces with the target.
  • Toggle "Зберігати історію замін" off → new switches no longer recorded.
  • Set retention to "1 тиждень", restart, verify older entries are pruned.
  • Long selected text (200+ chars) → preview in window truncated to 80 + "…".

🤖 Generated with Claude Code

… settings

Adds a dedicated NSWindow that opens on Spotlight launch (or app reopen) with
a sidebar split between "Дані" (clipboard history, replacement history,
recommendations) and "Налаштування" (existing General/Hotkeys/AutoFix/
Analytics/About tabs, now reused as sidebar sections instead of a TabView).
The standalone Settings scene is removed; both menu-bar entries open the new
window with a pre-selected section.

New persistence layer: ReplacementHistoryStore (singleton, JSONL in
Application Support, retention 1w/1m/3m/forever) records manual switches,
AutoFix applications, and AutoFix undos. RecommendationEngine analyzes that
history at Fibonacci thresholds (3, 5, 8, 13, 21) and surfaces three kinds of
suggestions: add a word to the allowlist (frequently undone autofix), create
a custom auto-replace rule (frequently performed manual switch), or add an
app to the blocklist (many undos in one bundle). Accepted suggestions mutate
the existing allowlist/blocklist or the new customAutoReplaceRules list,
which AutoFixController consults before the language scorer so personal
rules actually fire during typing.

Stored text is truncated to 80 characters to limit accidental capture of
passwords or long secrets. The whole feature is gated by a new
"Зберігати історію замін" toggle in General settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@rmarinsky rmarinsky added the release:minor New user-facing capability — bumps minor version label May 19, 2026
@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

🔖 On merge this PR will release v1.4.0 (release:minor).

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@rmarinsky has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 31 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86bb18e6-9fbe-4ba9-a643-c6f1aa4fa790

📥 Commits

Reviewing files that changed from the base of the PR and between 630c495 and f635151.

📒 Files selected for processing (7)
  • README.md
  • papuga/AppDelegate.swift
  • papuga/Core/HistoryWindowController.swift
  • papuga/Core/RecommendationEngine.swift
  • papuga/Models/ReplacementHistoryEntry.swift
  • papuga/Views/History/RecommendationsSectionView.swift
  • papuga/Views/Settings/AutoFixTab.swift
📝 Walkthrough

Walkthrough

This PR introduces a replacement history and recommendations feature that tracks text replacements (auto-fixes and manual switches), persists them to disk with configurable retention, generates actionable recommendations based on usage patterns, and exposes all this through a new unified history window. Custom auto-replace rules are added and integrated into the auto-fix flow.

Changes

Replacement History and Recommendations

Layer / File(s) Summary
Data Models and Persistence Keys
papuga/Models/ReplacementHistoryEntry.swift, papuga/Models/ReplacementHistoryRetention.swift, papuga/Models/CustomAutoReplaceRule.swift, papuga/Models/AppSettings.swift
ReplacementHistoryEntry defines three kinds of tracked replacements (auto-fix applied, auto-fix undone, manual switch) with truncation and layout/bundle tracking. ReplacementHistoryRetention provides localized retention presets (one week to forever). CustomAutoReplaceRule models user-created replacement rules with case-insensitive matching. New Defaults.Keys register five persisted settings for history enablement, retention, app-launch behavior, custom rules, and dismissed recommendations.
Replacement History Store
papuga/Core/ReplacementHistoryStore.swift
ReplacementHistoryStore is an @Observable singleton that records entries in-memory (with a configured cap) and persists them as newline-delimited JSON to an application-support directory. bootstrap() loads existing entries on startup, record(_:) conditionally appends entries (respecting the enabled flag), and pruneSync() applies time-based retention and enforces a persisted file-size cap before rewriting the JSONL atomically.
Recommendation Engine
papuga/Core/RecommendationEngine.swift
Recommendation enum models three types of suggestions (allowlist words, create custom rules, blocklist apps) with deduplication keys and tier-based ranking. RecommendationEngine.compute(...) analyzes replacement history to aggregate undone auto-fixes and manual switches, generates recommendations only when counts exceed thresholds, filters out items already in allowlists/blocklists/rules, suppresses dismissed items, and returns sorted results.
History Recording Integration
papuga/Core/AutoFixController.swift, papuga/Core/TextSwitchEngine.swift
AutoFixController.undo(...) and applyFix(...) now record entries into ReplacementHistoryStore. evaluateAndMaybeFix(...) checks Defaults[.customAutoReplaceRules] and applies matching rules early via a new applyCustomRule(...) method (which also records history). TextSwitchEngine.performSwitch(direction:) records manual switch replacements with source/target layout IDs and frontmost bundle ID.
History Window Controller
papuga/Core/HistoryWindowController.swift
HistoryWindowController singleton manages NSWindow lifecycle for the history UI. HistorySection enum defines four sections (clipboard, replacements, recommendations, settings) with localized titles and SF Symbols. showHistory(initialSection:) either updates an existing window via notification or creates and configures a new one with NSHostingView, size constraints, and a delegate that clears references on close.
History Window Views
papuga/Views/History/HistoryWindowView.swift, papuga/Views/History/ClipboardHistorySectionView.swift, papuga/Views/History/ReplacementsHistorySectionView.swift, papuga/Views/History/RecommendationsSectionView.swift
HistoryWindowView presents a NavigationSplitView with a sidebar listing sections and a detail pane that switches between four views. ClipboardHistorySectionView and ReplacementsHistorySectionView display history entries with search/filter, empty states, and clear actions. RecommendationsSectionView renders recommendation cards with tier badges and accept/dismiss buttons that update allowlist/blocklist/rule defaults and persist dismissals.
Settings UI and App Integration
papuga/Views/Settings/GeneralTab.swift, papuga/Views/Settings/AutoFixTab.swift, papuga/Views/MenuBarView.swift, papuga/AppDelegate.swift, papuga/papugaApp.swift
GeneralTab adds toggle/picker controls for history enablement, retention, and app-launch behavior plus a destructive clear action. AutoFixTab adds a form section for viewing and adding custom auto-replacement rules. MenuBarView replaces the settings button with "History and Recommendations" and "Settings" buttons that route to HistoryWindowController.shared. AppDelegate calls ReplacementHistoryStore.shared.bootstrap() during startup, conditionally opens the history window on launch, and implements applicationShouldHandleReopen(_:hasVisibleWindows:) to show history or onboarding on reopen. The legacy SettingsView and its Settings scene in PapugaApp are removed.

Sequence Diagrams

sequenceDiagram
  participant User
  participant ReplacementHistoryStore
  participant RecommendationEngine
  participant HistoryWindowView
  User->>HistoryWindowView: Open History Window
  HistoryWindowView->>ReplacementHistoryStore: fetch entries
  HistoryWindowView->>RecommendationEngine: compute(history, allowlist, rules, dismissed)
  RecommendationEngine->>RecommendationEngine: aggregate patterns, apply thresholds
  RecommendationEngine->>HistoryWindowView: return sorted recommendations
  HistoryWindowView->>User: Display history + recommendations
  User->>HistoryWindowView: Accept recommendation
  HistoryWindowView->>Defaults: append to allowlist/blocklist/rules
  HistoryWindowView->>Defaults: mark recommendation as dismissed
Loading
sequenceDiagram
  participant AutoFixController
  participant ReplacementHistoryStore
  participant lastFix
  participant Undo
  AutoFixController->>AutoFixController: evaluateAndMaybeFix(word)
  alt matches custom rule
    AutoFixController->>AutoFixController: applyCustomRule(rule)
  else standard path
    AutoFixController->>AutoFixController: applyFix(replacement)
  end
  AutoFixController->>ReplacementHistoryStore: record(autoFixApplied entry)
  AutoFixController->>lastFix: store replacement + layout IDs
  User->>Undo: trigger undo
  AutoFixController->>ReplacementHistoryStore: record(autoFixUndone entry with bundleID)
  AutoFixController->>lastFix: restore original
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • rmarinsky/papuga#4: Both PRs modify papuga/Core/AutoFixController.swift to record and track auto-fix operations; the related PR adds analytics-event and counter-logging infrastructure that this PR builds upon with history store integration.

Poem

🐰 Through history's maze we hop with care,
Recording fixes floating there—
When patterns dance, suggestions bloom,
We help our friends avoid their gloom!
Custom rules and lists divine,
Make replacements oh-so-fine! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title 'feat(history): unified history & recommendations window with embedded settings' directly and accurately summarizes the main change: introducing a unified window consolidating history, recommendations, and settings features.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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/history-recommendations-window

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 630c49591e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread papuga/Core/RecommendationEngine.swift Outdated
undoCount: count,
tier: tier(for: count)
)
if dismissedSet.contains(rec.dedupKey) && count < fibonacciThresholds.last! { continue }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress dismissed tier-5 recommendations

When a recommendation reaches the last threshold (21 or more repeats), dismissing it no longer has any effect because this check only skips dismissed items while count < 21; the same logic is repeated for rule and blocklist recommendations. In that scenario the user can click “Відхилити”, the dedup key is saved, but the card is recomputed and shown again immediately/forever because there is no higher tier to wait for.

Useful? React with 👍 / 👎.

Comment on lines +125 to +128
guard !customAutoReplaceRules.contains(where: {
$0.source.lowercased() == src.lowercased()
&& $0.target.lowercased() == tgt.lowercased()
}) else { 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.

P2 Badge Prevent conflicting custom rules with the same source

This duplicate check only rejects an identical source/target pair, so users can add another rule with the same source but a different target. At runtime AutoFixController applies Defaults[.customAutoReplaceRules].first(where: { $0.matches(word) }), so any later rule with the same source is saved and displayed but can never fire unless the earlier rule is deleted.

Useful? React with 👍 / 👎.

Comment thread papuga/AppDelegate.swift Outdated
Comment on lines +106 to +108
if Defaults[.openHistoryOnAppLaunch] {
AppLogger.action(logger, "Opening history window on launch")
HistoryWindowController.shared.showHistory()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Defer launch history until managers are configured

For returning users with openHistoryOnAppLaunch enabled (the new default), this opens the history window from applicationDidFinishLaunching, but the delegate's layoutManager and clipboardHistoryManager are only populated by PapugaApp.configureIfNeeded() from the MenuBarExtra .onAppear. If this path runs before that appearance callback, historyRootView constructs HistoryWindowView without the required LayoutManager/ClipboardHistoryManager environment values, and the initial clipboard section can hit SwiftUI's missing-environment runtime crash on launch.

Useful? React with 👍 / 👎.

@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: 5

🧹 Nitpick comments (1)
papuga/Views/Settings/AutoFixTab.swift (1)

125-131: ⚡ Quick win

Prevent conflicting rules with the same source text.

Current duplicate filtering blocks only identical (source,target) pairs, so one source can still map to multiple targets. That makes runtime behavior order-dependent and confusing.

Proposed fix
-                        guard !customAutoReplaceRules.contains(where: {
-                            $0.source.lowercased() == src.lowercased()
-                                && $0.target.lowercased() == tgt.lowercased()
-                        }) else { return }
+                        guard !customAutoReplaceRules.contains(where: {
+                            $0.source.lowercased() == src.lowercased()
+                        }) else { return }
🤖 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 `@papuga/Views/Settings/AutoFixTab.swift` around lines 125 - 131, The current
guard only prevents identical (source,target) pairs but allows the same source
to map to multiple targets; update the uniqueness check on
customAutoReplaceRules so it rejects any existing rule whose source.lowercased()
== src.lowercased() (regardless of target) before appending a new
CustomAutoReplaceRule(source: src, target: tgt); modify the guard that currently
uses contains(where:) to return early when any rule's source matches the new src
(use the same $0.source.lowercased() and src.lowercased() comparisons to locate
the code).
🤖 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 `@papuga/Core/HistoryWindowController.swift`:
- Around line 124-131: The current fallback branch instantiates
HistoryWindowView without required environment dependencies (layoutManager and
clipboardHistoryManager) which can crash; change the logic so HistoryWindowView
is only created when both AppDelegate.shared?.layoutManager and
AppDelegate.shared?.clipboardHistoryManager are non-nil (the existing if-let
branch), and remove the else that builds the view without environments; instead
return a safe fallback (e.g., an EmptyView or a small error/placeholder view)
and/or log the missing dependencies via AppDelegate or a logger so the missing
layoutManager/clipboardHistoryManager are detected without crashing (refer to
HistoryWindowView, AppDelegate.shared, layoutManager, clipboardHistoryManager,
initialSection).

In `@papuga/Core/RecommendationEngine.swift`:
- Around line 75-77: The aggregation uses raw bundleID as the key
(undoneByBundle[bundleID,...]) while recommendation identity uses lowercased
keys, causing split counts; normalize bundleID (e.g., let normalized =
bundleID.lowercased()) before using it as a dictionary key in the undoneByBundle
aggregation and in the other aggregation block around lines 121-130, and ensure
the same normalization is applied wherever dedupKey/id are formed so keys remain
consistent across counting and identity.

In `@papuga/Core/ReplacementHistoryStore.swift`:
- Around line 184-185: The assignments to keptLines and keptEntries use
trimmed.reversed() and trimmedEntries.reversed(), but reversed() returns a
ReversedCollection, not an Array; convert those results to arrays before
assigning (e.g., wrap reversed() calls with Array(...)) so keptLines ([String])
and keptEntries ([ReplacementHistoryEntry]) get proper Array values; update the
assignments that reference keptLines, keptEntries, trimmed and trimmedEntries
accordingly.

In `@papuga/Models/ReplacementHistoryEntry.swift`:
- Around line 63-66: The truncated(_:) logic in ReplacementHistoryEntry
currently preserves maxStoredCharCount characters then appends an ellipsis,
resulting in persisted length of maxStoredCharCount+1; change truncated(_ text:
String) so when text.count > maxStoredCharCount it takes
String(text.prefix(maxStoredCharCount - 1)) and appends "…" (ensure
maxStoredCharCount > 0 to avoid negative prefix length) so the total stored
length, including the ellipsis, equals maxStoredCharCount.

In `@papuga/Views/History/RecommendationsSectionView.swift`:
- Around line 80-83: The .addAppToBlocklist branch can append duplicates
differing only by case; update the check that uses
autoFixBlocklist.contains(bundleID) to perform a case-insensitive dedup by
comparing normalized forms (e.g., lowercased) of bundleID against the existing
entries in autoFixBlocklist, and only append when no case-insensitive match
exists (or alternatively normalize and store bundle IDs in a canonical case when
adding); target the case .addAppToBlocklist branch and the autoFixBlocklist
handling to implement this change.

---

Nitpick comments:
In `@papuga/Views/Settings/AutoFixTab.swift`:
- Around line 125-131: The current guard only prevents identical (source,target)
pairs but allows the same source to map to multiple targets; update the
uniqueness check on customAutoReplaceRules so it rejects any existing rule whose
source.lowercased() == src.lowercased() (regardless of target) before appending
a new CustomAutoReplaceRule(source: src, target: tgt); modify the guard that
currently uses contains(where:) to return early when any rule's source matches
the new src (use the same $0.source.lowercased() and src.lowercased()
comparisons to locate the code).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40175433-01d6-4c32-9437-ddd6c830a55a

📥 Commits

Reviewing files that changed from the base of the PR and between 7c22801 and 630c495.

📒 Files selected for processing (19)
  • papuga/AppDelegate.swift
  • papuga/Core/AutoFixController.swift
  • papuga/Core/HistoryWindowController.swift
  • papuga/Core/RecommendationEngine.swift
  • papuga/Core/ReplacementHistoryStore.swift
  • papuga/Core/TextSwitchEngine.swift
  • papuga/Models/AppSettings.swift
  • papuga/Models/CustomAutoReplaceRule.swift
  • papuga/Models/ReplacementHistoryEntry.swift
  • papuga/Models/ReplacementHistoryRetention.swift
  • papuga/Views/History/ClipboardHistorySectionView.swift
  • papuga/Views/History/HistoryWindowView.swift
  • papuga/Views/History/RecommendationsSectionView.swift
  • papuga/Views/History/ReplacementsHistorySectionView.swift
  • papuga/Views/MenuBarView.swift
  • papuga/Views/Settings/AutoFixTab.swift
  • papuga/Views/Settings/GeneralTab.swift
  • papuga/Views/Settings/SettingsView.swift
  • papuga/papugaApp.swift
💤 Files with no reviewable changes (2)
  • papuga/Views/Settings/SettingsView.swift
  • papuga/papugaApp.swift

Comment thread papuga/Core/HistoryWindowController.swift
Comment thread papuga/Core/RecommendationEngine.swift
Comment on lines +184 to +185
keptLines = trimmed.reversed()
keptEntries = trimmedEntries.reversed()

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 | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Swift, can a variable of type [String]be assigned directly fromsomeArray.reversed()without wrapping inArray(...)?

💡 Result:

No, a variable of type [String] cannot be assigned directly from the result of someArray.reversed without wrapping it in Array(...) or using another conversion method [1]. In Swift, the reversed method on an Array returns a ReversedCollection, not an Array [2][1]. A ReversedCollection is a lightweight wrapper that provides a view of the original array in reverse order without allocating new memory for the elements [2]. Because the types do not match—ReversedCollection<[String]> is not the same as [String]—the compiler will throw an error if you attempt a direct assignment [1]. To store the result as an Array, you must explicitly convert the collection [2][1]: let someArray = ["a", "b", "c"] let reversedArray = Array(someArray.reversed) // Correct: explicitly creates a new [String] Alternatively, you can use other initializers or methods that accept a sequence, such as: let reversedArray = String This conversion is necessary because it performs the actual allocation and copying of elements into a new array instance [2][1].

Citations:


🏁 Script executed:

# Find and verify the file exists
fd -type f -name "ReplacementHistoryStore.swift"

Repository: rmarinsky/papuga

Length of output: 231


🏁 Script executed:

# If found, check the specific lines (184-185)
if [ -f "papuga/Core/ReplacementHistoryStore.swift" ]; then
  sed -n '180,190p' "papuga/Core/ReplacementHistoryStore.swift" | cat -n
fi

Repository: rmarinsky/papuga

Length of output: 575


reversed() assignments will fail to compile.

Lines 184 and 185 assign ReversedCollection directly to [String] and [ReplacementHistoryEntry]. In Swift, reversed() returns a ReversedCollection, not an Array. Wrap with Array(...) to convert:

💡 Proposed fix
-            keptLines = trimmed.reversed()
-            keptEntries = trimmedEntries.reversed()
+            keptLines = Array(trimmed.reversed())
+            keptEntries = Array(trimmedEntries.reversed())
📝 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
keptLines = trimmed.reversed()
keptEntries = trimmedEntries.reversed()
keptLines = Array(trimmed.reversed())
keptEntries = Array(trimmedEntries.reversed())
🤖 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 `@papuga/Core/ReplacementHistoryStore.swift` around lines 184 - 185, The
assignments to keptLines and keptEntries use trimmed.reversed() and
trimmedEntries.reversed(), but reversed() returns a ReversedCollection, not an
Array; convert those results to arrays before assigning (e.g., wrap reversed()
calls with Array(...)) so keptLines ([String]) and keptEntries
([ReplacementHistoryEntry]) get proper Array values; update the assignments that
reference keptLines, keptEntries, trimmed and trimmedEntries accordingly.

Comment thread papuga/Models/ReplacementHistoryEntry.swift
Comment thread papuga/Views/History/RecommendationsSectionView.swift
rmarinsky and others added 3 commits May 19, 2026 14:59
…e update flow

PR #12 adds a unified history & settings window with Fibonacci-based
recommendations and custom auto-replace rules. The README's feature list
predates those changes; this commit brings it up to date and adds a short
note that the Homebrew cask may lag behind because Sparkle handles updates
inside the app.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tained

The Homebrew cask in rmarinsky/homebrew-tap is stuck at v1.0.1 while the
latest release is v1.3.2, and there is no intent to keep it in sync. Point
users directly at GitHub Releases instead; Sparkle handles updates inside
the app from then on.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ReplacementHistoryEntry.truncated: persisted text was 81 chars (80 + "…").
  Reserve a slot for the ellipsis so total length matches maxStoredCharCount.
- RecommendationEngine: dropped the `count < lastThreshold` escape hatch on
  dismissal. At tier 5 (count ≥ 21) the gate was always false, so dismissed
  recommendations resurfaced forever. Dismissal is now sticky across all tiers.
- RecommendationEngine: normalize bundleID with lowercased() before keying
  `undoneByBundle`, so mixed-case identifiers do not split the count or
  collide with the lowercased dedupKey.
- RecommendationsSectionView.accept(.addAppToBlocklist): case-insensitive
  dedup, store the normalized form to keep the blocklist canonical.
- AutoFixTab manual rule add: reject any rule whose source already exists,
  not just exact source/target pairs. At runtime AutoFixController fires only
  the first match by source, so duplicate-source rules were dead weight.
- AppDelegate: opening the history window on launch was racing with the
  SwiftUI scene that produces layoutManager / clipboardHistoryManager via
  MenuBarExtra.onAppear, leaving HistoryWindowView with nil environment
  values. Move the launch-time call into configure(...) where both managers
  are guaranteed to be populated.
- HistoryWindowController fallback: replace silent missing-env path with a
  visible warning placeholder so future regressions surface instead of
  crashing inside subviews.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@rmarinsky rmarinsky merged commit 6e41793 into main May 19, 2026
3 checks passed
@rmarinsky rmarinsky deleted the feat/history-recommendations-window branch May 19, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:minor New user-facing capability — bumps minor version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Неможливо повернути іконку Papuga в «Смуга меню»

1 participant