Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/secret_scanning.yml
Original file line number Diff line number Diff line change
@@ -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/**"
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 <App>" 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.
48 changes: 8 additions & 40 deletions Sources/TildeApp/App/StatusMenuHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}
Expand Down
11 changes: 10 additions & 1 deletion Sources/TildeApp/App/TildeSettingsSupportingViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand All @@ -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)
}
Expand Down
14 changes: 14 additions & 0 deletions Sources/TildeApp/App/TildeSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion docs/plans/screen-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -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 <static helper> --llama-sha256 <pin> \
--proof-gemma-model <gemma.gguf> --proof-qwen-model <qwen.gguf> \
--build-number <git rev-list --count HEAD> --version 0.1.0 \
--notary-profile <notarytool profile> --sign-identity <Developer ID SHA-1>
```

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.
9 changes: 9 additions & 0 deletions script/testdata/README.md
Original file line number Diff line number Diff line change
@@ -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`.
Loading