You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
• Add opt-in encrypted dictation history using libsodium with keys stored in KDE Wallet.
• Provide history UI for retention limits, per-entry/all deletion, and storage-location actions.
• Update navigation/footer patterns, tests, CI deps, docs, and refreshed screenshots.
Diagram
graph TD
UI["QML UI"] --> AC["AppController"] --> DH["DictationHistory"] --> ENC["libsodium AEAD"] --> FILE[("history.enc")]
DH --> KW{{"KDE Wallet"}}
UI --> DH
AC --> DI["DesktopIntegration"] --> FM{{"File manager"}}
subgraph Legend
direction LR
_ui["UI/Module"] ~~~ _file[("Local file")] ~~~ _ext{{"External"}}
end
Loading
High-Level Assessment
The following are alternative approaches to this PR:
1. Use QtKeychain / Secret Service abstraction
➕ Broader desktop support beyond KDE (GNOME Keyring, etc.)
➕ Less KDE-specific code in core history implementation
➖ Adds a new dependency and integration surface
➖ May reduce UX quality on Plasma vs direct KWallet APIs
➖ Still requires careful async/error handling like current approach
2. Store per-entry encrypted blobs instead of one encrypted snapshot
➕ Avoid rewriting the full history file on each save
➕ More resilient to partial corruption of a single entry
➖ More complex directory layout, pruning, and atomicity guarantees
➖ More filesystem operations and edge cases across platforms
3. Use KConfig/KConfigXT for history storage with a wallet-wrapped master key
➕ Leverages existing KDE stack and potentially simpler serialization
➕ Could allow incremental updates depending on backend
➖ Harder to guarantee authenticated-encryption boundaries and file-format safety limits
➖ Still needs atomic write strategy and corruption handling; complexity shifts rather than disappears
Recommendation: The PR’s approach (single authenticated-encrypted snapshot using libsodium + key in KDE Wallet) is a strong fit for Plasma: it keeps secrets out of the filesystem, fails closed when wallet/key/file are unavailable, and simplifies retention/deletion semantics. The main trade-off is rewriting the snapshot, but bounded by explicit retention limits and file-size caps; given the rigorous async/rollback handling and tests added here, this is a reasonable and maintainable design.
Files changed (33) +3458 / -387 · 8 not counted
Enhancement (9) +1661 / -153
AppController.cppPersist transcripts to encrypted history and expose storage actions+51/-2
Persist transcripts to encrypted history and expose storage actions
• Integrates DictationHistory lifecycle with settings and startup (including resuming pending deletion). Saves completed dictations into history and appends non-blocking warnings to delivery status when history can't save; adds invokables to reveal/open storage paths with error notifications.
AppController.hExpose DictationHistory to QML and add history/storage invokables+10/-1
Expose DictationHistory to QML and add history/storage invokables
• Adds a history QObject property plus invokable APIs for history enable/disable and opening storage locations; injects DictationHistory dependency for tests.
DictationHistory.cppImplement encrypted, retained dictation history with async persistence+769/-0
Implement encrypted, retained dictation history with async persistence
• Adds a QAbstractListModel-backed history store encrypted with XChaCha20-Poly1305 and keyed via KDE Wallet. Implements retention limits, per-entry/clear deletion, safe load/parse limits, atomic commits, failure rollback, and async save/load with queued snapshots and shutdown flush.
DictationHistory.hAdd DictationHistory model API and key-provider abstraction+157/-0
Add DictationHistory model API and key-provider abstraction
• Defines the HistoryKeyProvider interface and a DictationHistory list model with properties for enablement, busy/error state, retention settings, storage path, and recent entries for the UI.
Main.qmlAdd History page and refactor navigation, dialogs, and footers+540/-150
Add History page and refactor navigation, dialogs, and footers
• Adds a new History view (enable/disable, retention controls, per-entry and bulk deletion, storage path reveal) and a recent-history panel on the dictation page. Refactors navigation to include History, standardizes confirmation dialogs with Kirigami.PromptDialog, and moves status/warnings into consistent footers using FooterContainer.
FakeAppController.hExtend fake controller with history model and storage actions for QML tests+131/-0
Extend fake controller with history model and storage actions for QML tests
• Adds a FakeDictationHistory object and exposes it through FakeAppController, including toggles for busy/availability/reset-required and file-manager action stubs for UI tests.
tst_Main.qmlExpand QML UI tests for History view, footers, and compact navigation+245/-32
Expand QML UI tests for History view, footers, and compact navigation
• Updates view indices/navigation tests for the new History tab and adds coverage for history controls, accessibility, busy-state disabling, reset action visibility, footer margin consistency, and storage-path buttons.
README.mdDescribe optional encrypted history and update screenshots grid+15/-6
Describe optional encrypted history and update screenshots grid
• Updates project claims to reflect optional encrypted history (off by default), links to new history documentation, and refreshes the screenshot table to include the new History view.
CMakeLists.txtAdd DictationHistory sources, KF6::Wallet, and libsodium; add tests+31/-9
Add DictationHistory sources, KF6::Wallet, and libsodium; add tests
• Wires encrypted history into the build by linking KF6 Wallet and libsodium, adds new QML resources, installs new docs/screenshots, and introduces a dedicated history unit test target.
When loadEntries() rejects an oversized history file, it returns resetRequired=false, and the QML UI
only shows the reset action when resetRequired is true. This can leave users with a permanently
failing encrypted history file that cannot be deleted from within the app UI.
+ if (file.size() > maximumHistoryFileSize)+ return failure(i18n("The encrypted history file is too large to open safely."));
Evidence
The oversize error path uses the default resetRequired=false, while the QML explicitly gates the
reset action on resetRequired, and the enable/disable workflow only opens the disable+delete
dialog when history is enabled.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
If the history file is too large, `loadEntries()` returns a failure without setting `resetRequired=true`. The UI exposes the “Reset encrypted history” recovery action only when `resetRequired` is true, so oversize failures can become unrecoverable via the UI.
## Issue Context
- QML shows the reset action only when `appController.history.resetRequired` is true.
- The oversize path currently uses `failure(... /*resetRequired=*/false)`.
## Fix Focus Areas
- src/DictationHistory.cpp[631-635]
- src/qml/Main.qml[215-257]
## Suggested fix
- Change the oversize failure returns to set `resetRequired=true` (or introduce a distinct flag like `deletionRecommended`) so the UI provides a recovery path.
- Optionally, also add a UI action to delete existing encrypted history even when `enabled == false` but `available == false`, since `disableHistory(true)` can perform the cleanup.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
2. History save exceeds load cap✓ Resolved🐞 Bug☼ Reliability
Description
DictationHistory::persistEntries() writes magic+nonce+cipher without enforcing
maximumHistoryFileSize, so a large history can be saved successfully but later rejected by
loadEntries(). This can brick history on next start/enable until the user manually removes the
encrypted file/key outside the app.
+ QByteArray cipher(plain.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES, Qt::Uninitialized);+ unsigned long long cipherLength = 0;+ const int result = crypto_aead_xchacha20poly1305_ietf_encrypt(+ reinterpret_cast<unsigned char *>(cipher.data()), &cipherLength,
Evidence
The loader hard-rejects any file larger than 64MiB, but the saver constructs and commits an
arbitrarily-sized encrypted blob (based on JSON plaintext size) and the retention settings allow up
to 10k entries, making oversize files feasible.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
`DictationHistory::loadEntries()` refuses to open history files larger than `maximumHistoryFileSize` (64MiB), but `persistEntries()` does not bound the final encrypted payload size. This lets the app write a history file that it will later refuse to load.
## Issue Context
- The app allows up to 10,000 entries, and entry text is not size-limited.
- The size limit is enforced only at read-time, not at write-time.
## Fix Focus Areas
- src/DictationHistory.cpp[707-732]
- src/DictationHistory.cpp[629-635]
- src/DictationHistory.cpp[402-404]
## Suggested fix
- Before calling `commit(...)`, compute the final payload size (`magic.size() + nonce.size() + cipher.size()`).
- If it exceeds `maximumHistoryFileSize`, either:
- prune oldest entries until it fits (re-serialize/re-encrypt in a loop), or
- fail the save with a clear status telling the user to reduce retention / clear history.
- Ensure the behavior is consistent with the load-time limit so a successful save always remains loadable.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
View more (3) 3. Unpinned libsodium in CMake 📘 Rule violation☼ Reliability
Description
The new libsodium dependency is introduced via pkg_check_modules without an exact version or a
documented exception. This violates the dependency pinning requirement and can reduce build
reproducibility across environments.
PR Compliance ID 1 requires new/changed dependency entries to be pinned to an exact immutable
version (or have a documented exception). The added `pkg_check_modules(Sodium REQUIRED
IMPORTED_TARGET libsodium)` does not specify an exact version and has no adjacent exception
documentation.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
A new dependency (`libsodium`) is added without an exact version pin or an explicit documented exception.
## Issue Context
The compliance rule requires immutable version references where possible, or a nearby comment explaining why a range/unpinned dependency is required.
## Fix Focus Areas
- CMakeLists.txt[18-19]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
4. Non-ASCII ellipsis in i18n()📘 Rule violation✧ Quality
Description
New/modified string literals include the Unicode ellipsis character (…), violating the ASCII-only
string literal requirement. This can break tooling/pipelines that assume ASCII-only source strings.
PR Compliance ID 576985 requires all newly added/modified string literals to contain only ASCII
characters. The added status text Opening KDE Wallet… contains the non-ASCII ellipsis (…) in
both the C++ source and the updated translation catalog entry.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
String literals added in this PR include non-ASCII characters (e.g., the Unicode ellipsis `…`), which violates the English-only ASCII string literal policy.
## Issue Context
Examples include status strings used by `i18n(...)` and corresponding `.po` entries.
## Fix Focus Areas
- src/DictationHistory.cpp[249-249]
- po/x-test/kastword.po[241-254]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
5. Rich-text injection in history 🛡 Security issue⛨ SecurityCWE-79
Description
Attacker-influenced transcription text (from audio input) is rendered in a QML Label without forcing
plain-text, so HTML-like markup can be interpreted as rich text. This enables UI spoofing and may
trigger unintended link/resource handling when viewing dictation history.
The history entry transcript is bound directly to a Controls.Label (text: root.transcriptText)
without an explicit plain-text setting, allowing rich-text auto-detection to interpret markup-like
transcript content. The transcript originates from transcription output and is passed into history
storage and then bound into HistoryEntryCard for display.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
### Issue description
`HistoryEntryCard.qml` displays `transcriptText` inside a `Controls.Label` without setting `textFormat`. In Qt Quick, labels may interpret strings as rich text (via auto-detection), which can cause transcript content containing markup-like sequences to be rendered/handled unexpectedly.
### Issue Context
The transcript originates from `AppController::handleTranscriptionFinished()` and is stored/displayed later via the history model. Treat the transcript as untrusted display content and render it as plain text.
### Fix Focus Areas
- src/qml/HistoryEntryCard.qml[35-44]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
1. History save exceeds load cap✓ Resolved🐞 Bug☼ Reliability
Description
DictationHistory::persistEntries() writes magic+nonce+cipher without enforcing
maximumHistoryFileSize, so a large history can be saved successfully but later rejected by
loadEntries(). This can brick history on next start/enable until the user manually removes the
encrypted file/key outside the app.
+ QByteArray cipher(plain.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES, Qt::Uninitialized);+ unsigned long long cipherLength = 0;+ const int result = crypto_aead_xchacha20poly1305_ietf_encrypt(+ reinterpret_cast<unsigned char *>(cipher.data()), &cipherLength,
Evidence
The loader hard-rejects any file larger than 64MiB, but the saver constructs and commits an
arbitrarily-sized encrypted blob (based on JSON plaintext size) and the retention settings allow up
to 10k entries, making oversize files feasible.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
`DictationHistory::loadEntries()` refuses to open history files larger than `maximumHistoryFileSize` (64MiB), but `persistEntries()` does not bound the final encrypted payload size. This lets the app write a history file that it will later refuse to load.
## Issue Context
- The app allows up to 10,000 entries, and entry text is not size-limited.
- The size limit is enforced only at read-time, not at write-time.
## Fix Focus Areas
- src/DictationHistory.cpp[707-732]
- src/DictationHistory.cpp[629-635]
- src/DictationHistory.cpp[402-404]
## Suggested fix
- Before calling `commit(...)`, compute the final payload size (`magic.size() + nonce.size() + cipher.size()`).
- If it exceeds `maximumHistoryFileSize`, either:
- prune oldest entries until it fits (re-serialize/re-encrypt in a loop), or
- fail the save with a clear status telling the user to reduce retention / clear history.
- Ensure the behavior is consistent with the load-time limit so a successful save always remains loadable.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
When loadEntries() rejects an oversized history file, it returns resetRequired=false, and the QML UI
only shows the reset action when resetRequired is true. This can leave users with a permanently
failing encrypted history file that cannot be deleted from within the app UI.
+ if (file.size() > maximumHistoryFileSize)+ return failure(i18n("The encrypted history file is too large to open safely."));
Evidence
The oversize error path uses the default resetRequired=false, while the QML explicitly gates the
reset action on resetRequired, and the enable/disable workflow only opens the disable+delete
dialog when history is enabled.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
If the history file is too large, `loadEntries()` returns a failure without setting `resetRequired=true`. The UI exposes the “Reset encrypted history” recovery action only when `resetRequired` is true, so oversize failures can become unrecoverable via the UI.
## Issue Context
- QML shows the reset action only when `appController.history.resetRequired` is true.
- The oversize path currently uses `failure(... /*resetRequired=*/false)`.
## Fix Focus Areas
- src/DictationHistory.cpp[631-635]
- src/qml/Main.qml[215-257]
## Suggested fix
- Change the oversize failure returns to set `resetRequired=true` (or introduce a distinct flag like `deletionRecommended`) so the UI provides a recovery path.
- Optionally, also add a UI action to delete existing encrypted history even when `enabled == false` but `available == false`, since `disableHistory(true)` can perform the cleanup.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Validation
make screenshots— passed; five Plasma-themed screenshots inspectedmake validate— passed