# AGENTS.md This file provides guidance to agents when working with code in this repository. ## What this is Tide is an asynchronous prompt for the [Fish shell][fish], installed as a Fisher plugin. This is `plttn/tide`, a fork of `IlanCosman/tide`. All work here targets `plttn/tide` — never open PRs against upstream `IlanCosman/tide`. Everything is written in Fish script (`.fish`); there is no build step or compiled artifact. ## Commands All tasks are run via [mise][mise] (use `mise`, not `make`): - `mise run fmt` — format all `.fish` files with `fish_indent` - `mise run lint` — syntax-check all `.fish` files via `fish --no-execute` - `mise run install` — install Tide + test deps (fisher, clownfish) into the current Fish config - `mise run test [FILE...]` — run the littlecheck suite, or only the given test files (`scripts/test.fish`) - `mise run test-clean` — wipe the cached local test `HOME` (`scripts/test_clean.fish`) if the test env gets into a bad state ### Running a single test Tests are [littlecheck][littlecheck] `.fish` files under `tests/`, each a self-contained script with `# CHECK:` comments asserting stdout. Pass one or more to `mise run test` to run only those: ```fish mise run test tests/_tide_item_node.test.fish ``` Never run `python3 littlecheck.py` on a test file yourself. Test files set universal variables (`set -U tide_left_prompt_items ...`) and `funcsave` helpers, so outside the test `HOME` they overwrite the real fish config and break the user's prompt. `mise run install` also writes to the real config, so don't use it to verify changes either. A local run reuses a persistent `HOME` (`$XDG_CACHE_HOME/tide-test-home` or `~/.cache/tide-test-home`) so `fisher`/`clownfish` aren't reinstalled every run; each run still installs the working copy into it. In CI (`GITHUB_ACTIONS=true`) this caching is skipped and everything runs in a clean environment. Run the suite as a non-root user. `tests/_tide_item_pwd.test.fish` expects `/` to be unwritable (so it gets the unwritable-directory icon), and that check fails as root, e.g. in some containers and sandboxes. Test files mock external commands (e.g. `node`, `git`) via [clownfish][clownfish]'s `mock` function — see `tests/_tide_item_node.test.fish` for the pattern. `tests/test_setup.fish` defines `_tide_decolor` (strips ANSI codes for assertions) and sets `_tide_side` for right-prompt items. ### Debugging the async render path Timing bugs in the render path — a stale prompt, a missed repaint, two renders racing — usually can't be seen by calling `fish_prompt` from a script, since the bug is in *when* fish renders rather than in what one render prints. `scripts/prompt_probe/probe.py` drives a real interactive fish in a pty (isolated `HOME`, keystrokes, window resizes) and can log every render, dispatch and signal handler run. It is a manual tool, not part of `mise run test`; see `scripts/prompt_probe/README.md`. ## Architecture ### Item-based prompt composition The prompt is built from **items** — one function per prompt segment, named `_tide_item_.fish` in `functions/`. Each item function: - Reads config from user-exposed `tide__*` variables (colors, icons, thresholds) - Detects whether it's relevant (e.g. `_tide_item_node` checks `$_tide_parent_dirs` for `package.json` and that `node` is on PATH) and prints nothing if not - Calls `_tide_print_item ` (`functions/_tide_print_item.fish`) to emit its segment, which handles background/foreground coloring and separators between adjacent items based on `tide__prompt_items` Which items appear and in what order is controlled by the universal variables `tide_left_prompt_items` / `tide_right_prompt_items`, set by the configuration wizard or by hand. ### Rendering pipeline `functions/fish_prompt.fish` is the entry point Fish calls to render a prompt. On each render it: 1. Kicks off a **background job** (a second `fish -c ...` subprocess) that computes the actual prompt content by calling `_tide_1_line_prompt` or `_tide_2_line_prompt` (chosen at load time based on whether `newline` is in `$_tide_left_items`), renders it into a private scratch tmpfile named after its own pid, atomically renames it into place, then signals the parent with `SIGUSR1` 2. Immediately returns a (possibly stale) previous render so the shell never blocks 3. `_tide_refresh_prompt` (an `--on-signal SIGUSR1` handler) reads the tmpfile into a session-local global variable (`_tide_prompt_$fish_pid`) and calls `commandline -f repaint`, which re-renders `fish_prompt`/`fish_right_prompt` with the fresh content If `SIGUSR1` delivery can't be confirmed at shell init (some sandboxes/containers swallow signals — checked once via a `SIGUSR2` self-test), rendering falls back to fully synchronous instead of spawning a background job that could never notify completion. This async design is why Tide can afford expensive checks (e.g. full git status with untracked/modified/deleted counts) that would make a synchronous prompt feel slow. `fish_prompt.fish` itself is built with a large `eval "..."` block assembled once at shell-init time (varies by one-line vs two-line, frame-enabled vs not) rather than branching on every render, since load time matters more than per-render branching cost here. `_tide_1_line_prompt.fish` / `_tide_2_line_prompt.fish` iterate the configured items and concatenate their `_tide_print_item` output per side. ### VCS item internals `_tide_item_vcs.fish` dispatches between git and [jj (Jujutsu)][jj] backends: - `_tide_internal_jj_git.fish` walks up from `$PWD` looking for a `.jj` directory to decide if this is a jj repo (and whether `.disable-jj-prompt` opts back out) - If in a jj repo and the `jj` binary is available, `_tide_internal_vcs_jj.fish` renders; otherwise `_tide_internal_vcs_git.fish` handles git (and jj-repos-backed-by-git without the jj CLI) The git backend minimizes subprocess spawns (currently 3 git invocations) since each is a real fork/exec cost inside an already-forked background job. ### Configuration wizard `tide configure` (`functions/_tide_sub_configure.fish`) is a self-contained interactive TUI, not part of the render path. It walks a decision tree defined by `.fish` files under `functions/tide/configure/choices/{all,lean,classic,powerline,rainbow}/**` and `functions/tide/configure/functions/`, previewing changes live using `fake_columns`/`fake_lines` and no-op stand-ins for every `_tide_item_*` function. See `ARCHITECTURE.md` for the full choice flowchart. Chosen values are written out via `functions/tide/configure/configs/`. ### Subcommands `tide` (`functions/tide.fish`) dispatches to `_tide_sub_` functions (`configure`, `reload`, `bug-report`) — adding a subcommand means adding a new `_tide_sub_.fish`. ### Init / lifecycle hooks `conf.d/_tide_init.fish` wires up Fisher's `_tide_init_install` / `_tide_init_update` / `_tide_init_uninstall` events — first-install onboarding, version-migration shims (e.g. `_tide_migrate_vcs_prompt_items`), and cleanup of all `tide_*`/`_tide_*` functions and universal variables on uninstall. ## Conventions (Full detail in `CONTRIBUTING.md` — key points below.) - Style: prefer `test` over `[ ]`; prefer `&&`/`||` over `and`/`or` for simple conditionals, `if`/`else`/`else if` for anything more complex; prefer piping over command substitution when it doesn't require extra commands. - Naming: everything `snake_case`. User-facing names (variables, functions, files) are prefixed `tide_`; internal-only names are prefixed `_tide_`. Items live in `_tide_item_.fish`; subcommands in `_tide_sub_.fish`. - PR titles must follow [Conventional Commits][conventional-commits] (`[(scope)][!]: `) — PRs are squash-merged, so the PR title becomes the commit message on `main`, and [release-please][release-please] reads it to generate the changelog. This is CI-enforced. - Breaking changes (`!`) should be very rare, since they bump the major version. A new default look is a `feat` with a callout in the PR description. Where possible, give new defaults only to new installs: set them in `functions/tide/configure/configs/` and keep the old behavior when the variable is unset, so updates don't change existing setups. - Releases are fully automated by release-please; don't hand-edit `functions/tide.fish`'s version string or the floating `vN`/`vN.M` tags. [clownfish]: https://github.com/IlanCosman/clownfish [conventional-commits]: https://www.conventionalcommits.org/ [fish]: https://fishshell.com/ [jj]: https://jj-vcs.github.io/jj/latest/ [littlecheck]: https://github.com/ridiculousfish/littlecheck [mise]: https://mise.jdx.dev/ [release-please]: https://github.com/googleapis/release-please