This repository is the Linux-maintained fork of upstream macOS agterm.
It exists to maintain the GTK4/libadwaita Linux frontend while tracking upstream agterm as closely as possible.
When working in this repo:
- Maintain 1:1 feature parity with upstream macOS agterm wherever the Linux platform allows it.
- Keep shared controller/core behavior upstream-compatible and host-free.
- Keep Linux-specific code isolated to the Linux UI, Linux packaging, and platform adapters.
- Avoid product, protocol, or UX divergence unless Linux platform constraints require it.
- Treat Linux as a port of the same product, not a separate product.
- Preserve a small, reviewable downstream delta from upstream.
If a rule in the inherited macOS notes conflicts with this Linux-port guidance, keep the Linux release branch's fork principles first and preserve upstream compatibility everywhere else.
Read README.md for the product overview and fork policy.
Read ARCHITECTURE.md before changing module boundaries, terminal surfaces, control routing, or the
libghostty bridge.
The app is still agterm: a workspace/session terminal for many long-lived coding-agent or shell sessions. The Linux goal is to carry that model to GTK4/libadwaita without creating a separate product.
mastertracks upstreamumputun/agterm:master.linux-portis the maintained downstream branch.linux-portcombines upstreammasterwith the Linux UI, Linux packaging, CI, release workflow, and carried portable core fixes.- Linux releases are cut from
linux-port. - macOS builds, releases, and support belong to upstream agterm, not this fork.
- If a change is intended for upstream agterm, do it on a dedicated upstream-PR branch. Keep that branch free of Linux-port-only changes so opening a PR to upstream stays simple.
CHANGELOG.mdis release-only; do not touch it for ordinary feature work.
agtermCore/is shared Swift code and must remain host-free.agterm-linux/is the GTK4/libadwaita Linux application.linux/contains Linux-specific patches and support files.packaging/linux/contains Linux desktop and Flatpak packaging.scripts/*-linux.share Linux build, run, install, and packaging helpers.
Prefer portable, host-free changes in agtermCore/ only when they are compatible with upstream agterm.
Put Linux-only behavior behind Linux UI, packaging, or platform-adapter boundaries.
Do not place Linux-fork-only code in global shared areas or macOS-specific parts of the tree.
The Linux fork must operate inside its coding boundaries unless a change is deliberately portable and upstream-compatible.
agtermCore must not import app-host frameworks or terminal-rendering bindings.
Do not put GTK, libadwaita, AppKit, SwiftUI, Metal, GhosttyKit, libghostty FFI, CoreGraphics, or other
platform UI/runtime dependencies in agtermCore.
Keep model, persistence, naming, command validation, argument parsing, dispatch routing, response shaping,
and static catalogs in agtermCore when they can be expressed without host side effects.
Keep the Linux app target as a side-effect adapter for GTK/libadwaita, process, windowing, rendering, and
platform integration.
Use plain portable data types in shared code.
For geometry or UI state, prefer simple Swift structs backed by Double/Int and convert at the platform
boundary.
Keep LinuxGdkPolicy's GDK environment assignments the FIRST statements of main().
Nothing that initializes GTK or opens a display may be added above them, because GDK parses
GDK_DISABLE/GDK_DEBUG exactly once while GTK initializes and an earlier display open turns the
assignment into a silent no-op.
A new site that spawns a child process or launches a desktop handler owes the matching pre-launch restore
— see agterm-linux/docs/main-loop.md.
GTK owns the Linux main thread through g_application_run, and the GLib main loop drains neither
libdispatch's main queue nor the Swift Concurrency main-actor executor.
DispatchQueue.main.async/asyncAfter, Task { @MainActor } + Task.sleep, Timer.scheduledTimer, and
RunLoop scheduling therefore compile, run, and silently never fire on Linux — the failure mode is silence,
not a crash, so it survives review easily.
This applies to shared agtermCore code too, where the dispatch dependency is easiest to miss because the
file otherwise looks host-free.
- Schedule deferred main-actor work through
agtermCore'sMainTimerseam (MainTimer.schedule(after:_:), orDebounceron top of it), never a dispatch orTask.sleeptimer. - Hop to the main thread with
runOnMain(g_idle_add), neverDispatchQueue.main.async.runOnMainlives in the Linux target, so host-freeagtermCorehas no hop seam: from main-actor core code useMainTimer.schedule(after: 0), and add a hop seam alongsideMainTimeronly if a real need appears. - Keep
MainTimer's default Dispatch-based so upstream macOS semantics are unchanged; the Linux app swaps in the GLib implementation once, ininstallGLibMainTimer()at the top ofactivateApplication. DispatchQueue.globalis fine: global queues own their own threads.- Tests that swap the process-global
MainTimer.scheduleTimergo through thewithFakeMainTimerfixture — its synchronous closure is what keeps a parallel test's timer from landing on the fake. The Linux package cannot see that fixture, soAgtermLinuxTests/GLibMainTimerTests.swiftinstalls the real seam through its own synchronouswithGLibMainTimerhelper, under the same no-awaitcontract.
See agterm-linux/docs/main-loop.md for the full main-loop contract, the repro, and the unpersisted-preview
override pattern that live previews need.
When adding any user-visible feature or capability, evaluate how it should be driven over the control socket. Proactively propose and implement control coverage when meaningful:
- a
Commandcase and arguments inagtermCorecontrol protocol code, - dispatcher/control-server handling,
- an
agtermctlsubcommand or option, - protocol round-trip tests,
- end-to-end tests where practical.
A new user action is not done until the GUI/menu/keybinding path and control channel stay in sync. Skip control exposure only when it is genuinely meaningless, such as pure visual chrome with nothing to drive. Call out that exemption explicitly.
Whenever the Control API, keymap format, or window/workspace/session/pane model changes, update the bundled
agent skill under plugins/agterm/skills/agterm/.
That directory is the source of truth; never edit installed copies under user home directories.
Most maintainers and contributors are not GTK/libadwaita or macOS UI experts. When a UI request is non-standard, risky, or likely to fight the toolkit, push back gently first. Explain the trade-offs and offer the simpler native alternative. If the user still wants the custom approach, implement it.
Keep Linux UI behavior aligned with upstream macOS agterm where platform conventions allow it. Diverge only for Linux toolkit, desktop, packaging, accessibility, or distribution constraints.
For visual acceptance requests such as "show me", build and run an isolated dev instance for the user to test. Do not mutate the user's real state or live sessions. After launching a manual test instance, default to hands-off unless the user asks you to drive it.
Copy C string data into Swift-owned values before crossing actors, queues, or callback boundaries. Do not synchronously touch UI state from C callbacks. Hop to the appropriate main/UI context first.
Keep rendering demand-driven where possible. Avoid adding timers or polling loops for idle terminal rendering unless there is a measured need and a clear shutdown path.
When adopting a newer libghostty or changing bridge behavior, treat it as a deliberate compatibility change. Re-test terminal rendering, resize/font-size behavior, input, process lifecycle, and control-socket workflows.
The app must build, host-free swift test must stay green, and lint must pass after changes.
Use the repository scripts and Makefile as the source of truth for exact Linux commands.
For shared Swift package tests, run cd agtermCore && swift test or the repo wrapper if available.
For Linux app or packaging changes, use the Linux-specific scripts and Make targets documented in README.md
and the scripts themselves.
Manage file sizes for real. Source files should stay under the configured lint limits, and tests should stay within their expanded limits. When touching an already-long file, propose splitting or relocating code, but ask before restructuring. Do not raise lint limits just to fit new code.
When adding features, flags, keybindings, modes, packaging behavior, or release behavior, update the relevant
docs alongside code.
At minimum, consider README.md, Linux docs, packaging docs, bundled agent-skill docs, and any user-facing
site/docs mirror present in the repo.
Do not update CHANGELOG.md outside the release flow.
Anchor path and existence checks at the absolute repo root or use git -C <root> ....
Shell working directories drift between commands; do not claim a file or directory is missing from a relative
ls/find run in an unknown cwd.
When editing agent/project notes, use semantic line breaks. Write one sentence per line where practical, and split long bullets at natural clauses so future diffs stay small and reviewable.
Before touching CI, release, control API, settings, keymap, notifications, sidebar, windowing, libghostty,
or UI-test subsystems, check for matching notes under .claude/rules/ and follow the relevant guidance.
Prefer Linux-specific adaptations over copying macOS-only mechanics verbatim.