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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,32 @@ Chat with AI about selected text in a floating panel with streaming responses an
- Local session history with a configurable retention limit and controls to
delete individual sessions or clear all history

### Grammar Check

Check spelling, grammar, punctuation, and clarity in the text's original language,
including mixed-language passages. Choose British English, American English, or
accept both conventions.

Configure a provider and optional model override under **Settings > Grammar**,
then choose **Hotkey only** or **Automatic + hotkey**. Record a shortcut to check
selected text, or the current paragraph when nothing is selected. Automatic mode
checks the current paragraph after a typing pause and shows an issue indicator.

Review suggestions before accepting individual changes or applying all of them.
The panel offers **Grammar only**, **Improve clarity**, and **Rewrite**, with
optional Formal or Casual tone. Manual checks remember these choices; automatic
checks preserve your tone and use grammar and clarity suggestions. Rewrite shows
a full-text preview with an Original comparison.

The review stays open when you edit the source and offers **Recheck** before any
further changes can be applied. **Undo** restores the latest verified application
while the source is unchanged; starting a new check or changing the writing
profile clears this one-level undo state.

Checks are limited to 8,000 characters. When Accessibility exposes a selection
without a writable text range, results can be copied instead. Copy-based capture
uses the existing per-app Clipboard Fallback opt-in and only runs on a manual check.

### Custom LLM Actions

5 built-in prompt templates, plus support for creating your own:
Expand Down
103 changes: 103 additions & 0 deletions Sources/SelectionBarApp/GrammarSettingsTab.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import SelectionBarCore
import SwiftUI

struct GrammarSettingsTab: View {
@Bindable var settingsStore: SelectionBarSettingsStore
@Bindable var coordinator: GrammarCoordinator
@State private var showAppPicker = false

private var models: [String] {
switch settingsStore.grammar.providerID {
case "openai": settingsStore.availableOpenAIModels
case "openrouter": settingsStore.availableOpenRouterModels
default:
settingsStore.customLLMProviders.first { $0.providerId == settingsStore.grammar.providerID }?
.models ?? []
}
}

var body: some View {
Form {
Section {
Picker("Check grammar", selection: $settingsStore.grammar.mode) {
ForEach(GrammarTriggerMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
if settingsStore.grammar.mode != .off {
if !settingsStore.selectionBarEnabled {
Text("Enable SelectionBar in General to run grammar checks.")
.foregroundStyle(.secondary)
}
LabeledContent("Check shortcut") {
ShortcutRecorderField(keyBinding: $settingsStore.grammar.shortcut, width: 240)
}
if settingsStore.grammar.shortcut.isEmpty {
Text("Record a shortcut for manual checks.").foregroundStyle(.secondary)
}
if let error = coordinator.shortcutError { Text(error).foregroundStyle(.red) }
}
}
if settingsStore.grammar.mode != .off {
Section {
Picker("Provider", selection: $settingsStore.grammar.providerID) {
Text("Select provider").tag("")
let providers = settingsStore.availableChatProviders()
if !settingsStore.grammar.providerID.isEmpty,
!providers.contains(where: { $0.id == settingsStore.grammar.providerID })
{
Text("Unavailable provider").tag(settingsStore.grammar.providerID)
}
ForEach(providers) { provider in Text(provider.name).tag(provider.id) }
}
.onChange(of: settingsStore.grammar.providerID) { _, _ in
settingsStore.grammar.modelID = ""
}
Picker("Model", selection: $settingsStore.grammar.modelID) {
Text("Use provider default").tag("")
if !settingsStore.grammar.modelID.isEmpty,
!models.contains(settingsStore.grammar.modelID)
{
Text(settingsStore.grammar.modelID).tag(settingsStore.grammar.modelID)
}
ForEach(models, id: \.self) { model in
Text(model).tag(model)
}
}
.pickerStyle(.menu)
Picker("English convention", selection: $settingsStore.grammar.englishConvention) {
ForEach(GrammarEnglishConvention.allCases, id: \.self) { convention in
Text(convention.displayName).tag(convention)
}
}
if let error = coordinator.automaticError { Text(error).foregroundStyle(.red) }
} footer: {
Text(
"Checked text is sent to the selected provider. Languages are detected automatically.")
}
if settingsStore.grammar.mode == .automatic {
Section("Skip automatic checking in") {
ForEach(settingsStore.grammar.excludedApps) { app in
HStack {
Text(app.name)
Spacer()
Button("Remove", systemImage: "minus.circle.fill") {
settingsStore.grammar.excludedApps.removeAll { $0.id == app.id }
}
.labelStyle(.iconOnly)
.buttonStyle(.plain)
}
}
Button("Add Application", systemImage: "plus.circle") { showAppPicker = true }
}
}
}
}
.formStyle(.grouped)
.sheet(isPresented: $showAppPicker) {
ApplicationPickerSheet(existingBundleIDs: Set(settingsStore.grammar.excludedApps.map(\.id))) {
settingsStore.grammar.excludedApps.append(contentsOf: $0)
}
}
}
}
1 change: 1 addition & 0 deletions Sources/SelectionBarApp/MenuBarContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct MenuBarContentView: View {
Divider()

Button(action: {
SelectionBarAppManager.shared.appState.settingsPresentation += 1
openSettings()
NSApplication.shared.activate(ignoringOtherApps: true)
Task { @MainActor in
Expand Down
Loading
Loading