neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
AI-powered pull request review agent for macOS and Linux — reviews the PRs you're asked to review and tracks your own PRs towards merge. Uses Claude, Gemini, Codex, or OpenCode under the hood, posts everything back as your GitHub account, and keeps you informed via a native menu-bar app or a Flutter Web UI.
Heimdallm runs in the background and does three things, in parallel, at your configured poll interval:
1. PR reviews
Watches the PRs where you're requested as a reviewer, runs an AI code review, and submits it to GitHub as your account. No copy-pasting, no manual prompting.README 전문 보기 ↗AI-powered pull request review agent for macOS and Linux — reviews the PRs you're asked to review and tracks your own PRs towards merge. Uses Claude, Gemini, Codex, or OpenCode under the hood, posts everything back as your GitHub account, and keeps you informed via a native menu-bar app or a Flutter Web UI.
Heimdallm runs in the background and does three things, in parallel, at your configured poll interval:
Watches the PRs where you're requested as a reviewer, runs an AI code review, and submits it to GitHub as your account. No copy-pasting, no manual prompting.
Watches the PRs you authored or are assigned to and works out exactly what is stopping each one from merging — which check is failing, which reviewer is waiting, which conversation is unresolved. The My PRs tab groups them into needs your action, ready to merge and waiting on others, flags the ones that went quiet, and drops merged ones after a day. It nudges you without getting in the way: a badge, a tray section and one digest notification a day (per-PR alerts are opt-in). Watching never writes to GitHub. Optionally, merge tracking moves them along too: arm GitHub's auto-merge, update branches that fall behind, have the agent resolve conflicts, and merge when everything is green. Every automation is off by default.
A Flutter Web UI (:3000) with responsive sidebar/rail/drawer navigation for Activity, My PRs, Repositories, Organizations, Prompts, Agents, Stats, and Instances, plus live Settings and Server/Logs screens. Opens alongside the daemon in Docker mode.
review-requested:@me on GitHub and submits reviews as your account{diff} {title} {author} {comments} placeholders, managed from the web UI at /prompts (/agents remains a compatibility alias)high severity always triggers REQUEST_CHANGES; with never_approve_with_issues enabled, any finding at or above the configured threshold does too, otherwise the review approvesmake up spins up daemon + web UI for server/team deploymentsFrom a checkout of this repository, use the Make targets (preferred):
make install-macos # install the latest release
make install-macos RELEASE=v0.7.5 # install a specific release
make uninstall-macos # remove app + service; keep user state
make uninstall-macos PURGE=1 # also delete config, history, and logs
Run these targets as your normal user, never with sudo make. If
/Applications needs elevated permissions, the installer requests a sudo
ticket once and limits it to the app-bundle operations. A pre-existing
LaunchAgent is migrated to the installed bundle; uninstall always removes the
LaunchAgent so launchd cannot keep retrying a missing executable.
Migration of a loaded, enabled LaunchAgent regenerates its canonical plist from the bundled daemon, so manual plist edits are discarded. If the LaunchAgent was unloaded or disabled, install changes only its daemon executable path and preserves the other plist keys.
Normal uninstall preserves config, review history, logs, and the macOS
Keychain credential. PURGE=1 irreversibly removes only the canonical
~/.config/heimdallm, ~/.local/share/heimdallm, and
~/Library/Logs/heimdallm directories. It does not follow custom config/data
path overrides, and it still preserves the Keychain credential. Remove that
separately, if desired, with
security delete-generic-password -s heimdallm -a github-token.
An empty PURGE means normal uninstall; any non-empty value other than exact
PURGE=1 is rejected before the LaunchAgent, app, or user data is changed.
Without a checkout, use the manual DMG fallback:
Heimdallm-vX.Y.Z.dmg from Releasesxattr -cr /Applications/Heimdallm.app
Requires: macOS 13+ (Apple Silicon or Intel),
ghCLI authenticated (gh auth login).
Download from Releases and install for your distro:
| Package | Distros | Command |
|---|---|---|
.deb | Ubuntu, Debian, Mint, Pop!_OS | sudo dpkg -i heimdallm_X.Y.Z_amd64.deb |
.rpm | Fedora, RHEL, openSUSE | sudo rpm -i heimdallm-X.Y.Z-1.x86_64.rpm |
.AppImage | Arch, NixOS, any distro | chmod +x Heimdallm-X.Y.Z-x86_64.AppImage && ./Heimdallm-X.Y.Z-x86_64.AppImage |
Installs to /opt/heimdallm/ with a desktop entry and /usr/bin/heimdallm symlink.
Requires:
ghCLI authenticated (gh auth login). Token stored via GNOME Keyring / KDE Wallet (secret-tool), or~/.config/heimdallm/.tokenas fallback.
Signed release builds check for updates once a day. When one is available,
Heimdallm shows a dashboard banner and a system-tray action that updates the
complete app plus daemon with one click (.deb / .rpm may show the normal OS
authorization prompt). Docker/web and source-built installs remain managed by
their deployment or checkout.
Developers with a clone of this repo who want a native install without the full Flutter toolchain on the host:
make install-linux # build via Docker, install to ~/.local/
make uninstall-linux # remove app, keep config + data
make uninstall-linux PURGE=1 # also wipe ~/.config + ~/.local/share state
make install-linux runs verify-linux (which builds the heimdallm-verify Docker image), extracts the Flutter bundle + Go daemon from the image, and stages them into ~/.local/opt/heimdallm/ with a ~/.local/bin/heimdallm symlink, a desktop entry, and four icon sizes under ~/.local/share/icons/hicolor/. No sudo. No host Flutter SDK. The first install also seeds ~/.config/heimdallm/.token from $GITHUB_TOKEN or gh auth token so the app launches cleanly from the OS app launcher on first run.
Requires: Docker running,
ghCLI authenticated (or$GITHUB_TOKENexported), Ubuntu 22.04 / Debian 12+ / Fedora / Arch or similarly current distro. Same binary-compatibility envelope as the CI-built.deb.
The terminal client is distributed as heimdallm-cli via Homebrew and GitHub Releases, and ships inside the desktop installers (/Applications/Heimdallm.app/Contents/MacOS/heimdallm-cli on macOS, /usr/bin/heimdallm-cli from the .deb / .rpm). It connects to a running daemon and supports status checks, PR lists, manual review triggers, live event following, stats, config inspection, and a Bubble Tea dashboard.
brew install theburrowhub/tap/heimdallm-cli
# or download a heimdallm-cli_* archive from GitHub Releases
heimdallm-cli dashboard
Updating the CLI. heimdallm-cli update downloads the latest release for your OS/arch, verifies it against the release's cli-checksums.txt, and replaces the binary (the previous one is kept as heimdallm-cli.bak and restored if the new one fails to start). heimdallm-cli update --check or heimdallm-cli version --check only report whether a newer release exists. Homebrew installs should use brew upgrade theburrowhub/tap/heimdallm-cli; the copy bundled with the desktop app is updated by the desktop app. The daemon and the desktop app update on their own; Docker deployments update via make up-build or an image pull.
Interactive commands print a one-line notice when a newer release exists (checked at most once per interval, never applied automatically). Tune it in ~/.config/heimdallm/cli.toml:
[auto_update]
enabled = true # default; false silences the notice
check_interval = "24h" # default
channel = "stable" # "stable" (releases only) or "latest" (includes pre-releases)
For headless/server deployment, Heimdallm runs as a Docker container with all four AI CLIs bundled. The repository ships Make wrappers around docker compose so you don't have to remember the compose path.
repo scope (or public_repo for public-only). Two options:
gh CLI: just run gh auth token to print the token you already have — no need to create a new PAT.claude setup-token on your host (interactive — opens a browser) and copy the sk-ant-oat… token it prints. This becomes CLAUDE_CODE_OAUTH_TOKEN. No billing setup required if your Anthropic subscription covers Claude Code.ANTHROPIC_API_KEY.GEMINI_API_KEY. Or reuse your host's browser OAuth — see Reusing your host's AI authentication.OPENAI_API_KEY or CODEX_API_KEY.OPENROUTER_API_KEY.cp docker/.env.example docker/.env
# Quick-fill GITHUB_TOKEN from your existing gh auth (skip if you made a PAT):
echo "GITHUB_TOKEN=$(gh auth token)" >> docker/.env
Then open docker/.env in your editor and set at minimum:
HEIMDALLM_AI_PRIMARY — claude | codex | gemini | copilot | cursor_cli | opencode | openrouterHEIMDALLM_REPOSITORIES — owner/repo1,owner/repo2 (or leave empty if using HEIMDALLM_DISCOVERY_TOPIC)Filling in the Claude credential:
Subscription (recommended if you have Claude Max / Pro / Team):
$(...)):
claude setup-token
It opens a browser, prints some ANSI text, and finally emits a line that starts with sk-ant-oat….sk-ant-oat… line and paste it manually into docker/.env:
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat...
ANTHROPIC_API_KEY empty. Do not set bare = true in config.toml — it disables OAuth.Why you can't inline this:
claude setup-tokenis interactive and writes prompts + colour codes to stdout.$(claude setup-token)captures all of it and corrupts.env.
API key (pay-as-you-go): paste your Anthropic key directly:
ANTHROPIC_API_KEY=sk-ant-...
Leave CLAUDE_CODE_OAUTH_TOKEN empty.
For non-Claude providers see Reusing your host's AI authentication (Gemini OAuth reuse) or just set the relevant *_API_KEY variable from the Prerequisites list.
See docker/.env.example for every supported variable including topic-based discovery, and web UI settings.
make up # daemon + web UI (recommended)
# or:
make up-daemon # daemon only, no web UI
# after `git pull` on main:
make up-build # same as `make up` but rebuilds from local source
make up-build-daemon # same as `make up-daemon` but rebuilds from local source
make up refuses to start if docker/.env is missing and prints the exact copy-from-template command. The web container waits for the daemon's healthcheck before accepting traffic, so the first UI request never races a half-initialised daemon.
Tracking
main?make upreuses the last cached image. The published:lateston GHCR only refreshes when release-please cuts a tag, so between a PR merge and the next release you'll still be running the old binary. Usemake up-buildto rebuild from your checkout.
Port already in use? If
make upfails withbind: address already in usefor port3000(common collision: a local Next.js / Vite / Grafana dev server) or7842, override the host-side port indocker/.envand re-run:echo "HEIMDALLM_WEB_PORT=3100" >> docker/.env # for the web UI echo "HEIMDALLM_PORT=7843" >> docker/.env # for the daemon make upTo see what's holding the port:
lsof -iTCP:3000 -sTCP:LISTEN.
make ps # show container status
curl http://localhost:7842/health # -> {"status":"ok"}
Then open the web UI:
open http://localhost:3000xdg-open http://localhost:3000http://localhost:3000 manually.The web container reads the daemon's API token from the shared heimdallm-data volume automatically — no manual copy needed.
After make up completes, the target prints a summary of which AI credentials were picked up from docker/.env and flags the optional knobs (full-repo analysis, topic discovery) that are currently off. Use it as a checklist before opening the UI.
By default the AI agent reviews a PR using only the diff it gets from the GitHub API. That's enough for most reviews. If you want the agent to explore the surrounding code (grep sibling files, read the call graph, check imports, etc.), you need to give it a directory on the daemon's side to cd into.
On Docker the daemon is containerised, so "a directory on the daemon's side" means a path inside that container. Mount your host repos root into the container as /home/heimdallm/repos (under the daemon user's home — the executor rejects workdirs outside it) via an env var:
# In docker/.env
HEIMDALLM_LOCAL_DIR_BASE=/Users/you/projects # macOS / Linux
# or
HEIMDALLM_LOCAL_DIR_BASE=/home/you/code
On desktop the daemon reads the same env var directly (no mount involved) and supports a comma-separated list for multi-workspace setups:
export HEIMDALLM_LOCAL_DIR_BASE=/Users/you/projects,/Users/you/other-org
make dev
make down && make up
Then in the web UI, go to a repo's detail screen and under Local directory enter the path inside the container — typically /home/heimdallm/repos/<repo-name> (matching the sub-directory of your host's projects root). When unset, the compose file mounts an empty placeholder there so the feature is a silent no-op and doesn't break anything.
The mount is read-only. The agent can read every file under that directory but cannot modify your working tree.
On the desktop app this is automatic: the Browse button opens the native picker and stores the host path directly — no mount needed.
make logs # tail logs from daemon + web
make logs-daemon # daemon only
make restart # restart both containers
make down # stop and remove containers (data volume persists)
make setup # (optional) copy the daemon API token into docker/.env.
# Only needed if you want to call the daemon's HTTP API
# from outside Docker (scripts, CI, `curl` from the host).
# The web UI does NOT need this — it reads the token from
# the shared volume automatically.
The Docker image is published to ghcr.io/theburrowhub/heimdallm:latest on every release — make up pulls it automatically when the build: contexts haven't changed locally.
If you already authenticate the AI CLIs on your host, you can reuse those
credentials inside Docker instead of pasting an API key into .env. The
daemon runs its own bundled CLIs inside the container — it never shells
out to the binaries on your host — but the container can read the same
OAuth tokens the host CLIs wrote.
Claude Code (Max / Pro / Team subscription):
claude setup-token
Copy the long-lived token it prints.docker/.env:
CLAUDE_CODE_OAUTH_TOKEN=<paste token>
Leave ANTHROPIC_API_KEY empty. docker-compose.yml already forwards
CLAUDE_CODE_OAUTH_TOKEN into the container.bare = true in config.toml — it disables OAuth and
forces API-key mode.Gemini CLI (browser OAuth):
gemini on your host the normal way. Credentials land in
~/.gemini/.docker/docker-compose.yml, uncomment the line under the heimdallm
service's volumes: block:
- ~/.gemini:/home/heimdallm/.gemini:ro
The mount is read-only, so the container cannot clobber your host
credentials.GEMINI_API_KEY empty in docker/.env.Codex / OpenCode: host-auth reuse is not wired up yet — use API keys
(OPENAI_API_KEY, CODEX_API_KEY, OPENROUTER_API_KEY) in docker/.env.
make up picks up the changes on the next start. Full reference (including
Vertex AI service-account mode for Gemini) in
docs/e2e-test-guide.md.
Instead of listing every repository in HEIMDALLM_REPOSITORIES, you can tag
the repos you want Heimdallm to watch with a GitHub topic and let the
daemon discover them:
HEIMDALLM_DISCOVERY_TOPIC=heimdallm-review
HEIMDALLM_DISCOVERY_ORGS=your-org,your-other-org # required
HEIMDALLM_DISCOVERY_INTERVAL=15m # optional, defaults to 15m
Any non-archived repo inside one of the listed orgs that carries the topic is
merged into the monitored set on each discovery cycle. Transient Search API
errors fall back to the last known-good list, so an outage never empties the
set silently. The static HEIMDALLM_REPOSITORIES list keeps working — the
two sources are merged (deduplicated) at poll time.
See LLM-HOW-TO-INSTALL.md for a step-by-step guide suitable for Claude Code, shell scripts, or any automation tool.
On first launch Heimdallm detects your gh CLI token automatically and sets itself up.
The Go daemon (heimdalld, port 7842) is the engine. It polls GitHub for PRs, dispatches reviews to the configured AI CLI, posts them back to GitHub, tracks your own PRs towards merge, and broadcasts state to any connected UI over SSE.
Three first-party UIs talk to it over HTTP:
.dmg / Linux packages.3000, served by Nginx, ships as a second Docker container alongside the daemon.heimdallm-cli via Homebrew and GitHub Releases.Flutter app ─┐
Web UI ─┼──→ HTTP / SSE ──→ heimdalld ──→ GitHub API
CLI / TUI ─┘ │
├──→ PR review pipeline ──→ POST /reviews
└──→ Merge tracking ──→ auto-merge / branch update / merge
│
claude / codex / gemini / copilot / cursor / opencode CLI
In Docker mode the daemon runs standalone with the web UI container as an optional-but-recommended companion (brought up by default with make up). Configuration is via environment variables (HEIMDALLM_*) or a mounted config.toml, and can be edited live from the web UI Settings screen at /config.
One daemon is the default and needs nothing configured. When you want more — a second machine, a container per team, a box with the GPU — you can run several daemons and manage them from a single UI. One acts as the hub:
Flutter app ─┐ ┌──→ heimdalld (srv-a) ──→ GitHub
Web UI ─┼──→ heimdalld (hub) ──────┼──→ heimdalld (srv-b) ──→ GitHub
CLI / TUI ─┘ registry └──→ heimdalld (hub) ──→ GitHub
routing rules
config push
Repositories and organizations are routed to specific instances, so each daemon polls GitHub for everything but only acts on what it owns — that partition is what keeps two daemons from reviewing the same PR. Operations you trigger by hand can additionally be spread round-robin across the fleet.
The UI never connects to a remote daemon directly: it reads every instance through the hub's authenticated proxy, so the browser keeps one origin and one credential. Shared configuration (prompts, review and merge policy, polling) pushes to every instance in one action; ports, tokens and local paths stay per-machine.
make up-instance NAME=b PORT=7843 # another daemon on this host
heimdallm-cli instances # the fleet, with health and versions
heimdallm-cli routing # who owns what
See §15 Multiple Instances for the full configuration, the routing precedence, and what does and does not propagate.
gh CLI authenticatedTwo workflows — pick based on what you're changing:
Flutter desktop app or daemon Go code — native toolchain:
git clone https://github.com/theburrowhub/heimdallm && cd heimdallm
make dev # builds daemon + launches Flutter app in debug mode
make dev compiles the daemon, embeds it, and launches the Flutter app with HEIMDALLM_DAEMON_PATH pointing to the local binary. First run detects your gh token and writes ~/.config/heimdallm/config.toml.
Web UI or Docker deployment — containerised:
cp docker/.env.example docker/.env # fill in GITHUB_TOKEN + provider key
make up # daemon + web UI
make logs # follow both services
For iterating on the Flutter Web bundle against a running daemon:
make build-web # compile Flutter → web/; then `make up-build` to bake into the Nginx image
The production web image requires BuildKit: its Dockerfile-specific
.dockerignore is what keeps the repository-root context minimal. The Make
wrappers enforce this requirement. For direct validation, run these commands
from the repository root (flutter_app/assets links to the shared assets/
directory):
sh docker/scripts/test-web-build-context.sh
docker buildx build --load --target build -t heimdallm-web-build:test -f flutter_app/Dockerfile.web .
DOCKER_BUILDKIT=1 docker compose -f docker/docker-compose.yml build web
Do not use the legacy docker build backend for this Dockerfile: it ignores
flutter_app/Dockerfile.web.dockerignore and can transfer unrelated repository
files to the builder.
CLI / TUI — terminal client against a running daemon:
make build-cli
make dev-daemon
make dev-cli
make test-cli
make dev-cli launches heimdallm-cli dashboard against http://localhost:7842 by default. Set HEIMDALLM_HOST and HEIMDALLM_TOKEN when targeting another daemon.
make test # Run daemon Go + Flutter + CLI tests on the host
make test-docker # Run Go tests inside a pinned Docker image (EDR-safe)
make build-cli # Build cli/bin/heimdallm-cli
make test-cli # Run CLI module tests on the host
make lint-cli # Run CLI module vet checks
make dev-cli # Run the CLI dashboard against localhost:7842
make dev-daemon # Run daemon only (debug API at localhost:7842)
make dev-stop # Stop the running daemon
make up # Docker: bring up daemon + web UI
make up-build # Docker: same as `up`, rebuild from local source (use on main)
make up-daemon # Docker: daemon only
make up-build-daemon # Docker: same as `up-daemon`, rebuild from local source
make down # Docker: stop and remove containers
make logs # Docker: follow all services
make restart # Docker: bounce both containers
make ps # Docker: container status
make setup # Docker: copy daemon API token into docker/.env (optional)
make release-local # Build signed + notarized DMG and publish GitHub release
make install-macos # macOS: install the latest release into /Applications
make uninstall-macos # macOS: remove app + LaunchAgent (add PURGE=1 to wipe user state)
make test-install-macos # macOS installer: run portable shell logic tests
make verify-linux # Linux: build the heimdallm-verify Docker image (full CI pipeline inside)
make run-linux # Linux: launch the Flutter bundle from inside the Docker image (X11)
make install-linux # Linux: native install to ~/.local/ (no sudo, Docker-built)
make uninstall-linux # Linux: remove the native install (add PURGE=1 to wipe config+data)
Working on a laptop with corporate EDR (Elastic Security, CrowdStrike, …)? Use
make test-dockerinstead ofmake testfor the Go suite.go testcompiles ephemeral*.testbinaries that EDR heuristics flag as malware; running inside Docker keeps those artefacts in the container's tmpfs. SeeAGENTS.mdfor the full rationale and the hardening details to share with your security team.
heimdallm/
├── daemon/ Go background service (port 7842)
│ └── internal/
│ ├── github/ GitHub API client (PRs, diffs, reviews)
│ ├── executor/ AI CLI runner (claude, codex, gemini, copilot, cursor, opencode)
│ ├── pipeline/ PR-review orchestration
│ ├── gitops/ Git plumbing for merge-tracking branch updates
│ ├── mergetrack/ Merge-readiness evaluator and reconciler
│ ├── discovery/ Topic-based repo auto-discovery
│ ├── store/ SQLite (prs, reviews, agents)
│ ├── scheduler/ Poll loop, grace windows
│ ├── server/ HTTP + SSE API
│ └── keychain/ Host credential storage
├── cli/ Terminal client and TUI (separate Go module)
│ ├── cmd/heimdallm-cli/ Cobra entrypoint
│ └── internal/
│ ├── api/ Daemon HTTP + SSE client
│ ├── cli/ Commands and config loading
│ └── tui/ Bubble Tea dashboard
├── flutter_app/ macOS / Linux / Web UI
│ ├── lib/
│ │ ├── features/
│ │ │ ├── dashboard/ Reviews tab (My Reviews / My PRs)
│ │ │ ├── merge_tracking/ My PRs tab and per-check breakdown
│ │ │ ├── repositories/Repo management + per-repo config
│ │ │ ├── agents/ Review prompt library
│ │ │ └── stats/ Review statistics
│ │ └── core/
│ │ ├── api/ HTTP + SSE client
│ │ └── setup/ First-run setup, token detection
│ └── web/ Flutter Web build output (served by Nginx on port 3000)
├── docker/ Docker deployment
│ ├── Dockerfile Daemon image (Go + Node + 4 AI CLIs)
│ ├── docker-compose.yml daemon + web UI services
│ ├── .env.example Environment variable reference
│ └── scripts/ Local test runner (smoke/github/e2e)
├── AGENTS.md Policy for AI coding agents working on this repo
├── .github/workflows/ Tests, build, release-please, Docker publish
└── Makefile Both native and Docker workflows
See the Configuration Guide for full reference of all settings, environment variables, and deployment options.
# Automatic version bump from conventional commits
# (Actions → "Tag release" → Run workflow)
# Or manually
make release-local VERSION=v1.2.3
The tag.yml workflow analyses commits since the last tag:
feat!: / BREAKING CHANGE → major bumpfeat: → minor bumpfix:, refactor:, etc. → patch bumpPushing the tag triggers build.yml, which builds, signs, notarizes (if Developer ID configured), and publishes the DMG to GitHub Releases.
Alternatively, release-please (via release.yml) automates this: it reads conventional commits, opens a Release PR with changelog, and on merge creates the tag + GitHub Release. This also triggers the Docker image build and push to ghcr.io/theburrowhub/heimdallm.
Each component also has its own release line: release-please bumps daemon/vX.Y.Z, gui/vX.Y.Z and cli/vX.Y.Z from the commits under daemon/, flutter_app/ and cli/, next to the unified vX.Y.Z installer, which stays the latest release and lists the component versions it bundles in versions.json. See docs/release-pipeline.md.
On macOS, the daemon's local_dir_base feature calls os.Stat() on
candidate paths for each monitored repository (to auto-detect local
clones). If HEIMDALLM_LOCAL_DIR_BASE is set to a broad path like $HOME,
the stat probes can touch TCC-protected directories (~/Music,
~/Downloads, ~/Pictures, etc.), causing macOS to show permission
dialogs — even though Heimdallm has no interest in those folders.
Fix: set HEIMDALLM_LOCAL_DIR_BASE to the specific directory (or
directories) where your repos actually live:
# Good — scoped to your actual code directories
export HEIMDALLM_LOCAL_DIR_BASE=~/projects,~/work
# Bad — triggers TCC prompts for every protected subfolder of $HOME
export HEIMDALLM_LOCAL_DIR_BASE=~
The native file picker (Browse button in repo settings) may also
trigger similar prompts when macOS enumerates recent/default locations.
This is standard NSOpenPanel behavior and not specific to Heimdallm.
MIT