From 8478007a22a773a96f3be8713b1a94c13045ef6d Mon Sep 17 00:00:00 2001 From: r3dbars Date: Wed, 2 Sep 2026 12:52:40 -0500 Subject: [PATCH] release prep: a five-item menu bar, Exact Screen Text in Privacy & Data, synthetic test data marked for secret scanning, changelog and release guide, one local path removed The menu bar now shows state, today's line, Pause, Ignore (when there is one), Open Tilde, and Quit. The model picker and the Accessibility prompt live in the Tilde window, where the rest of the settings already are. Co-Authored-By: Claude Fable 5.1 --- .github/secret_scanning.yml | 7 +++ CHANGELOG.md | 34 +++++++++++++ Sources/TildeApp/App/StatusMenuHost.swift | 48 ++++--------------- .../App/TildeSettingsSupportingViews.swift | 11 ++++- .../TildeApp/App/TildeSettingsViewModel.swift | 14 ++++++ docs/plans/screen-memory.md | 2 +- docs/releasing.md | 31 ++++++++++++ script/testdata/README.md | 9 ++++ 8 files changed, 114 insertions(+), 42 deletions(-) create mode 100644 .github/secret_scanning.yml create mode 100644 CHANGELOG.md create mode 100644 docs/releasing.md create mode 100644 script/testdata/README.md diff --git a/.github/secret_scanning.yml b/.github/secret_scanning.yml new file mode 100644 index 000000000..fac340e95 --- /dev/null +++ b/.github/secret_scanning.yml @@ -0,0 +1,7 @@ +# The redaction evaluator's corpus deliberately contains secret-shaped +# strings (fake AWS keys, GitHub tokens, private-key blocks) so the +# redaction rules can be scored. They are generated by +# script/generate_redaction_corpus.py and are not credentials. +paths-ignore: + - "script/testdata/**" + - "Tests/**" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ff8635b8b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to Tilde. Versions follow the app's +`CFBundleShortVersionString`; the build number is the commit count at packaging. + +## Unreleased + +### Added +- Screen Memory master toggle in Privacy & Data; turning it off drops every held + screen snapshot and the menu names the state instead of calling it a pause. +- One-click "Ignore " in the menu bar. +- Your Tilde leads with keystrokes saved, kept-after-30-seconds, helpful + streaks, and how often Tilde held back and why. +- A text-free outcome ledger that records every shown ghost and every silent + model opportunity with a terminal reason and a configuration fingerprint. +- Mid-word model continuation, chained accept, punctuation-boundary requests, + and the shorter Electron reveal floor, all gated to the 9B preview profile. +- The trained personal model persists across restarts. + +### Changed +- Streaming ghosts survive turning on Personal suggestions. +- The sensitive-scene guard covers documents beside the composer. +- The cleaner, echo, and grounding checks prepare their context once per request. +- License: MIT (unchanged); a trademark notice covers the name and logo. + +### Fixed +- Register, candidate source, and per-thousand-character denominators in the + outcome ledger. +- A restore path that would have discarded pre-upgrade personal history. +- A newer keyboard failing every response from an older app. + +## 0.1.0 — unreleased beta + +First public beta. See README.md for status and requirements. diff --git a/Sources/TildeApp/App/StatusMenuHost.swift b/Sources/TildeApp/App/StatusMenuHost.swift index 2cee0d39a..b5023cd08 100644 --- a/Sources/TildeApp/App/StatusMenuHost.swift +++ b/Sources/TildeApp/App/StatusMenuHost.swift @@ -143,9 +143,7 @@ final class StatusMenuHost: NSObject { private var pauseItem: NSMenuItem? private var ignoreAppItem: NSMenuItem? private var setupOrTildeItem: NSMenuItem? - private var accessibilityItem: NSMenuItem? private var modelPickerItem: NSMenuItem? - private var productionModelChoiceItems: [TildeModelChoice: NSMenuItem] = [:] private var modelChoiceItems: [PreviewModelChoice: NSMenuItem] = [:] private var h01Item: NSMenuItem? @@ -176,19 +174,12 @@ final class StatusMenuHost: NSObject { todayItem = addAction(to: menu, "0 words with Tilde today", #selector(openYourTilde(_:))) menu.addItem(.separator()) - if TildeProductProfile.current == .production { - let picker = NSMenuItem(title: "Model", action: nil, keyEquivalent: "") - let submenu = NSMenu(title: "Model") - for choice in TildeModelChoice.allCases { - let choiceItem = addAction(to: submenu, choice.displayName, #selector(selectModel(_:))) - choiceItem.representedObject = choice.rawValue - productionModelChoiceItems[choice] = choiceItem - } - picker.submenu = submenu - menu.addItem(picker) - modelPickerItem = picker - menu.addItem(.separator()) - } else if TildeProductProfile.current == .modelPreview { + // The menu is for the three things a writer does in a hurry: see the + // state, pause, and ignore the app in front of them. Everything else + // — the model choice, permissions, data — lives in the Tilde window. + // The Model Preview build keeps its research controls here because + // it is a research build. + if TildeProductProfile.current == .modelPreview { let picker = NSMenuItem(title: "Model", action: nil, keyEquivalent: "") let submenu = NSMenu(title: "Model") for choice in PreviewModelChoice.allCases { @@ -221,7 +212,6 @@ final class StatusMenuHost: NSObject { // alike. Hidden until some other app has actually been frontmost. ignoreAppItem = addAction(to: menu, "Ignore App", #selector(ignoreForegroundApplication(_:))) ignoreAppItem?.isHidden = true - accessibilityItem = addAction(to: menu, "Exact Screen Text", #selector(enableExactScreenText(_:))) setupOrTildeItem = addAction(to: menu, "Open \(productName)", #selector(openTilde(_:))) menu.addItem(.separator()) addAction(to: menu, "Quit \(productName)", #selector(quit(_:)), key: "q") @@ -253,12 +243,7 @@ final class StatusMenuHost: NSObject { pauseItem?.title = presentation.primaryAction ?? "" pauseItem?.isHidden = presentation.primaryAction == nil setupOrTildeItem?.title = "Open \(TildeProductProfile.current.displayName)" - if let active = appDelegate.selectedProductionModel() { - modelPickerItem?.title = "Model: \(active.shortName)" - for (choice, item) in productionModelChoiceItems { - item.state = choice == active ? .on : .off - } - } else if let active = appDelegate.selectedPreviewModel() { + if let active = appDelegate.selectedPreviewModel() { modelPickerItem?.title = "Model: \(active.shortName)" for (choice, item) in modelChoiceItems { item.state = choice == active ? .on : .off @@ -284,11 +269,6 @@ final class StatusMenuHost: NSObject { } h01Item?.state = settings.h01BlockRandomizationEnabled ? .on : .off - let exactTextGranted = AccessibilityPermission.isGranted() - accessibilityItem?.title = exactTextGranted - ? "Exact Screen Text: On" - : "Enable Exact Screen Text (Accessibility)…" - accessibilityItem?.state = exactTextGranted ? .on : .off refreshIcon(for: state.iconAppearance) tildeWindow?.refresh() @@ -355,15 +335,6 @@ final class StatusMenuHost: NSObject { /// Exact screen text reads the focused window's text through the /// Accessibility API instead of OCR. Asks the system prompt, then opens /// the pane so a previously dismissed prompt still has a path. - @objc private func enableExactScreenText(_ sender: Any?) { - guard let appDelegate else { return } - if !AccessibilityPermission.isGranted() { - appDelegate.requestAccessibilityAccess() - if !AccessibilityPermission.isGranted() { appDelegate.openAccessibilitySettings() } - } - refresh() - } - /// Adds the last frontmost non-Tilde app to the shared excluded-apps /// list. Same list, same writer, same normalization as the Settings /// "Add App…" panel, so the exclusion applies everywhere it applies @@ -385,10 +356,7 @@ final class StatusMenuHost: NSObject { @objc private func selectModel(_ sender: NSMenuItem) { guard let rawValue = sender.representedObject as? String else { return } - if TildeProductProfile.current == .production, - let choice = TildeModelChoice(rawValue: rawValue) { - appDelegate?.selectProductionModel(choice) - } else if let choice = PreviewModelChoice(rawValue: rawValue) { + if let choice = PreviewModelChoice(rawValue: rawValue) { appDelegate?.selectPreviewModel(choice) } } diff --git a/Sources/TildeApp/App/TildeSettingsSupportingViews.swift b/Sources/TildeApp/App/TildeSettingsSupportingViews.swift index 661c012aa..edd1975d2 100644 --- a/Sources/TildeApp/App/TildeSettingsSupportingViews.swift +++ b/Sources/TildeApp/App/TildeSettingsSupportingViews.swift @@ -27,6 +27,15 @@ struct PrivacyAndDataView: View { .fixedSize(horizontal: false, vertical: true) } + LabeledContent("Exact Screen Text", value: model.exactScreenTextGranted ? "On" : "Off") + if !model.exactScreenTextGranted { + Button("Enable Exact Screen Text…") { model.enableExactScreenText() } + Text("With Accessibility access Tilde reads the window's text exactly instead of recognizing it from pixels. Reading only; Tilde never types through Accessibility.") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + Button("Apps Tilde Ignores…") { showIgnoredApps = true } Button("Manage Local Data…") { showLocalData = true } } @@ -40,7 +49,7 @@ struct PrivacyAndDataView: View { } } .formStyle(.grouped) - .frame(width: 460, height: 380) + .frame(width: 460, height: 440) .sheet(isPresented: $showIgnoredApps) { IgnoredAppsView(model: model) } diff --git a/Sources/TildeApp/App/TildeSettingsViewModel.swift b/Sources/TildeApp/App/TildeSettingsViewModel.swift index 32718fee6..2b8868250 100644 --- a/Sources/TildeApp/App/TildeSettingsViewModel.swift +++ b/Sources/TildeApp/App/TildeSettingsViewModel.swift @@ -161,6 +161,7 @@ final class TildeSettingsViewModel: ObservableObject { launchAtLoginEnabled = settings.launchAtLoginEnabled screenMemoryEnabled = settings.screenMemoryEnabled screenRecordingGranted = ScreenRecordingPermission.isGranted() + exactScreenTextGranted = AccessibilityPermission.isGranted() personalizationEnabled = personalHistory.isEnabled selectedProductionModel = appDelegate?.selectedProductionModel() selectedPreviewModel = appDelegate?.selectedPreviewModel() @@ -281,6 +282,19 @@ final class TildeSettingsViewModel: ObservableObject { : "Tilde cannot see what you are replying to, so it is not suggesting anything. Turn this back on to get suggestions again." } + /// Exact screen text: with macOS Accessibility permission Tilde reads + /// the focused window's text exactly instead of recognizing it from + /// pixels. Reading only; Tilde never inserts text through Accessibility. + @Published private(set) var exactScreenTextGranted = false + + func enableExactScreenText() { + if !AccessibilityPermission.isGranted() { + appDelegate?.requestAccessibilityAccess() + if !AccessibilityPermission.isGranted() { appDelegate?.openAccessibilitySettings() } + } + exactScreenTextGranted = AccessibilityPermission.isGranted() + } + func enableScreenAccess() { setScreenMemoryEnabled(true) if !ScreenRecordingPermission.isGranted(), !ScreenRecordingPermission.request() { diff --git a/docs/plans/screen-memory.md b/docs/plans/screen-memory.md index 9249ca04b..cbc87f7c5 100644 --- a/docs/plans/screen-memory.md +++ b/docs/plans/screen-memory.md @@ -36,7 +36,7 @@ Non-negotiables that survive from the old covenant: ## Prior art to port, not rebuild -- July lineage (checkout `/Users/redbars/redbars/code/transcripted-autocomplete-lab`): +- July lineage (the earlier `transcripted-autocomplete-lab` checkout): `script/ocr_probe.swift` (capture+Vision spike), `script/ocr_eval.py` (context-arm eval), docs/quiz-lessons.md "Screen context (OCR) is worth a lot" (EM@1 14.5%→17.2% with OCR context). Port concepts and prompt shapes; diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 000000000..eacabe84d --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,31 @@ +# Releasing Tilde + +One notarized build per release, published on the GitHub Releases page, tagged +`vMAJOR.MINOR.PATCH` (add `-beta.N` while the README says beta). + +1. Update `CHANGELOG.md`: move "Unreleased" under the new version with the date. +2. Run the single release driver with human-reviewed pins (see + `script/package_app.sh --help`); it builds, signs, runs the release proof + with both pinned models, notarizes, and writes `dist/Tilde.dmg`, + `dist/Tilde.zip`, and the checksum file: + + ```bash + ./script/package_app.sh --llama-server --llama-sha256 \ + --proof-gemma-model --proof-qwen-model \ + --build-number --version 0.1.0 \ + --notary-profile --sign-identity + ``` + +3. Tag the packaged commit and publish: + + ```bash + git tag -a v0.1.0-beta.1 -m "Tilde 0.1.0 beta 1" + git push origin v0.1.0-beta.1 + gh release create v0.1.0-beta.1 dist/Tilde.dmg dist/Tilde.zip dist/*.sha256 \ + --title "Tilde 0.1.0 beta 1" --notes-file <(sed -n '/^## 0.1.0/,/^## /p' CHANGELOG.md) + ``` + +4. Record the helper and model pins from the proof directory in the release + notes. The DMG never contains a model; first run downloads the pinned asset. + +The models themselves are not release artifacts and are never attached. diff --git a/script/testdata/README.md b/script/testdata/README.md new file mode 100644 index 000000000..834d259ca --- /dev/null +++ b/script/testdata/README.md @@ -0,0 +1,9 @@ +# Synthetic test data + +Everything in this directory is generated, not collected. The redaction +evaluation corpus contains strings shaped like real secrets — AWS access keys, +GitHub tokens, session tokens, private-key blocks, email addresses — precisely +so the redaction rules can be scored against them. None is a working +credential; the addresses use reserved example domains; the corpus is written +by `script/generate_redaction_corpus.py` from a fixed seed. GitHub secret +scanning is told to skip this directory in `.github/secret_scanning.yml`.