Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ jobs:
# letting us keep a macOS 14 deployment target in build.sh.
run: sudo xcode-select -s /Applications/Xcode_26.2.app || sudo xcode-select -s /Applications/Xcode.app

- name: Run logic and screenshot tests
run: |
chmod +x run-tests.sh run-screenshot-tests.sh
./run-tests.sh
./run-screenshot-tests.sh

- name: Import signing certificate
if: env.SIGN_IDENTITY != '' && env.IS_RELEASE_EVENT == 'true'
env:
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ Skills worth writing for this repo specifically (not yet authored):

## Build & run

Agent-run commands must always have an explicit bounded timeout. Use 30 seconds for quick inspection commands, 2–4 minutes for tests, and at most 5 minutes for a full build. If a command exceeds its bound, stop it, report the timeout, and investigate rather than leaving an unbounded process running.

```bash
./build.sh # produces build/Mindle.app
open build/Mindle.app
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,11 @@ Requires **macOS 14+** and **Xcode Command Line Tools** (`xcode-select --install

### Workflow
- **Tabs and multi-window** — open many files in one window (`⌘O` adds a tab) or pop a new window with `⌘N`. Each tab carries its own scroll, theme, font scale, and collaborator registry. `⌘W` closes the active tab when more than one is open, otherwise the window.
- **Open a folder** — `⌘⌥O` opens a local directory directly.
- **SSH profiles** — the ssh button at the top left opens the favorite profile from `~/Library/Application Support/Mindle/ssh-profiles.yaml` or **Mindle → Settings → SSH Profiles**.
- **Open remote URLs** — `⌘⇧L` opens any `http(s)` URL pointing at raw Markdown (a GitHub raw link, a hosted doc) as a tab. Annotations on URL documents persist locally keyed by URL hash, so re-opening the same URL brings the annotations back.
- **Open from Clipboard** — `⌘⇧V` opens the pasteboard contents as a Markdown tab. Use this when the source is behind a login your browser already handles (internal GitLab raw, Confluence, anything auth-walled): copy the raw Markdown there, paste it here. Tabs are content-addressed, so re-pasting identical text re-opens the same tab with its prior annotations.
- **File browser** — scoped sidebar tree of every `.md` and `.txt` in the current folder (`⌘⇧F`). Never escapes upward.
- **File browser** — scoped sidebar tree of `.md`, `.markdown`, `.mdown`, `.mkd`, `.txt`, and `.pdf` files (`⌘⇧F`). Additional configuration is available under **Mindle → Settings → File Browser**.
- **Find in document** — live search with match count, `⌘F` / `⌘G` / `⌘⇧G`.
- **Live reload** — external edits (vim, an agent, Dropbox, anything) re-render automatically. Bursty writes are debounced; scroll position is preserved. Sidecar changes (a teammate's annotation arriving via shared folder or `git pull`) flow in the same way.
- **Diff-on-reload** — when an external write changes the active file, Mindle renders the change as a Word-style track-changes overlay you can ✓ Keep or ✗ Revert per chunk, or whole-document with `⌘⌥⏎` / `⌘⌥⌫`. Diffs run a second pass at word granularity inside each line, so you see just the changed words struck or underlined — not the whole line.
Expand Down Expand Up @@ -114,6 +116,7 @@ See [Agent Collaboration](#agent-collaboration) below for setup.
| Shortcut | Action |
|----------|--------|
| `⌘O` | Open a file (adds a tab if a window is open) |
| `⌘⌥O` | Open a folder in the file sidebar |
| `⌘N` | New window |
| `⌘W` | Close active tab (or window, when only one tab is open) |
| `⌘F` | Find in document |
Expand Down
28 changes: 21 additions & 7 deletions Resources/web/reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1250,16 +1250,30 @@
if (!src) return { url: null };
if (src.startsWith("data:")) return { url: src };
if (/^https?:/i.test(src)) return { blocked: true };
const pathSrc = decodeImagePath(src);
if (/^file:\/\//i.test(src)) {
const path = src.replace(/^file:\/\//i, "");
return { url: "mindle-file://" + path };
const path = pathSrc.replace(/^file:\/\//i, "");
return { url: "mindle-file://" + encodeImagePath(path) };
}
if (src.startsWith("/")) {
return { url: "mindle-file://" + encodeURI(src) };
if (pathSrc.startsWith("/")) {
return { url: "mindle-file://" + encodeImagePath(pathSrc) };
}
if (!baseDir) return { url: pathSrc };
const resolved = resolveRelativePath(baseDir, pathSrc);
return { url: "mindle-file://" + encodeImagePath(resolved) };
}

function decodeImagePath(src) {
const path = src.split(/[?#]/, 1)[0];
try {
return decodeURIComponent(path);
} catch (_) {
return path;
}

function encodeImagePath(path) {
return path.split("/").map(encodeURIComponent).join("/");
}
if (!baseDir) return { url: src };
const resolved = resolveRelativePath(baseDir, src);
return { url: "mindle-file://" + encodeURI(resolved) };
}

function resolveRelativePath(base, rel) {
Expand Down
23 changes: 23 additions & 0 deletions Sources/mindle/BrowserDisplaySettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

enum BrowserDisplaySettings {
static let showGitChangesKey = "mindle.fileBrowser.showGitChanges"
static let showLastEditedKey = "mindle.fileBrowser.showLastEdited"
static let highlightActiveFileKey = "mindle.fileBrowser.highlightActiveFile"

static func showGitChanges(defaults: UserDefaults = .standard) -> Bool {
enabledByDefault(showGitChangesKey, defaults: defaults)
}

static func showLastEdited(defaults: UserDefaults = .standard) -> Bool {
enabledByDefault(showLastEditedKey, defaults: defaults)
}

static func highlightActiveFile(defaults: UserDefaults = .standard) -> Bool {
enabledByDefault(highlightActiveFileKey, defaults: defaults)
}

private static func enabledByDefault(_ key: String, defaults: UserDefaults) -> Bool {
defaults.object(forKey: key) as? Bool ?? true
}
}
Loading