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