311 lines
17 KiB
Markdown
311 lines
17 KiB
Markdown
# Fido Project Overview
|
||
|
||
Fido is a minimal, interactive terminal coding assistant. It connects an OpenRouter-hosted model to a deliberately small set of workspace tools, then presents the conversation and tool activity in a full-screen terminal UI.
|
||
|
||
The project is written in modern JavaScript as native ES modules. It runs directly from source with Node.js; there is no transpiler, bundler, or JSX build step.
|
||
|
||
## What Fido Can Do
|
||
|
||
- Inspect and search a project without leaving the terminal.
|
||
- List directories, find files by name, search file contents, and read files in chunks.
|
||
- Apply precise file edits, create files and directories, and remove files or directory trees.
|
||
- Run shell commands after explicit user approval.
|
||
- Search the web and extract web pages through Tavily when configured.
|
||
- Ask the user clarifying questions or request help from a human expert.
|
||
- Stream model reasoning and tool notifications into a scrollable terminal interface.
|
||
- Navigate project files from the prompt and insert whole-file, single-line, or line-range mentions.
|
||
- Keep mutation and conversation state isolated per workspace, outside repositories when installed.
|
||
- Compact large tool payloads between turns to reduce token usage and avoid retaining stale file contents.
|
||
|
||
## Runtime Architecture
|
||
|
||
```text
|
||
bin/fido
|
||
├─ loads Fido configuration
|
||
├─ selects per-workspace state
|
||
└─ starts src/fido.js
|
||
└─ runRepl()
|
||
├─ builds the system prompt
|
||
├─ drives LLMOrbiter + OpenRouter
|
||
├─ executes tools from src/tools/
|
||
└─ drives the Ink UI through src/ui/
|
||
|
||
model tool call
|
||
└─ tool registry
|
||
├─ workspace path validation
|
||
├─ filesystem, shell, or Tavily operation
|
||
└─ structured result returned to the model and shown in the UI
|
||
```
|
||
|
||
### Main Modules
|
||
|
||
| Module | Responsibility |
|
||
| --- | --- |
|
||
| `bin/fido` | Installed/source launcher; loads user configuration and creates per-workspace state paths. |
|
||
| `src/fido.js` | Process entry point; checks for the required API key and starts the REPL. |
|
||
| `src/repl.js` | Owns conversation state, provider turns, cancellation, tool events, and transcript compaction. |
|
||
| `src/turn-state.js` | Closed enum of connection states plus the pure `(state, event) -> state` reducer that drives the command-bar label. |
|
||
| `src/config.js` | Loads `.env` files and resolves the model, workspace root, and state locations. |
|
||
| `src/system-prompt.js` | Builds the workspace-aware model instructions sent at session start. |
|
||
| `src/commands.js` | Implements local slash commands without spending a model turn. |
|
||
| `src/slash-commands.js` | Defines and resolves the built-in command registry. |
|
||
| `src/tools/index.js` | Registers the model-facing tools and their UI notification renderers. |
|
||
| `src/tools/*.js` | Implements individual filesystem, shell, web, and interaction tools. |
|
||
| `src/sandbox.js` | Resolves relative tool paths and rejects absolute paths or lexical traversal outside the workspace. |
|
||
| `src/dir-listing.js` | Shared `.gitignore`-aware directory walking and pagination. |
|
||
| `src/glob.js` | Small shared glob dialect used by name and content searches. |
|
||
| `src/lines.js` | Shared line splitting with editor-compatible trailing-newline behavior. |
|
||
| `src/compact.js` | Shrinks old patch arguments and invalidates file reads made stale by later patches. |
|
||
| `src/undo.js` | Persists a newest-first log describing mutations made by tools. |
|
||
| `src/suspend.js` | Saves and restores manually suspended conversation snapshots. |
|
||
| `src/transcript.js` | Formats assistant, user, tool, error, command, and recalled-message output. |
|
||
| `src/file-browser.js` | Provides testable file-browser navigation and mention-formatting helpers. |
|
||
| `src/ui/index.js` | Exposes an imperative UI singleton to the REPL and tools. |
|
||
| `src/ui/store.js` | External, subscriber-based state store for prompts, history, status, and usage totals. |
|
||
| `src/ui/app.js` | Root Ink application and fixed terminal layout. |
|
||
| `src/ui/main-input.js` | Main input, slash-command completion, and `Ctrl+O` file/line browser. |
|
||
|
||
## Conversation Flow
|
||
|
||
1. Configuration resolves the workspace root using the first positional argument, then `ROOT_DIR`, then the current directory.
|
||
2. The REPL loads undo and suspend state and mounts the Ink UI.
|
||
3. It builds a system prompt containing the workspace root, a short root listing, tool contracts, and operating rules.
|
||
4. The user submits a normal message or a local slash command.
|
||
5. Normal input is sent to `LLMOrbiter`, which uses the OpenRouter provider and advertises the registered tools to the model.
|
||
6. A provider turn can iterate up to 20 times, allowing the model to call tools and continue reasoning before returning a final response.
|
||
7. Tool calls and results are rendered in the transcript; the structured result is also returned to the model.
|
||
8. Between user turns, Fido compacts large patch arguments and replaces pre-patch file reads that are no longer current.
|
||
9. The resulting message array remains in memory for the session. It can be saved explicitly with `/suspend`.
|
||
|
||
## Model-Facing Tools
|
||
|
||
All currently registered tools are defined in `src/tools/index.js`.
|
||
|
||
| Tool | Purpose and notable behavior |
|
||
| --- | --- |
|
||
| `list_files` | Lists one directory, optionally recursively, with `.gitignore` support and pagination. Dot-directories are hidden unless requested. |
|
||
| `find_files` | Recursively finds files or directories by a simple name glob. Slashless patterns also match basenames. |
|
||
| `read_file` | Reads a workspace file as line-based content and supports zero-indexed `seek`/`limit` chunking. |
|
||
| `ripgrep` | Performs a pure-JavaScript regular-expression content search. It skips binary files, files over 2 MiB, ignored paths, and hidden directories by default. Long matching lines are capped. |
|
||
| `patch` | Applies ordered create/overwrite or exact first-match search/replace operations. Supports `skiplines`, CRLF adaptation, partial-progress reporting, and undo records. |
|
||
| `mkdir` | Recursively creates a directory and records its creation. |
|
||
| `rm` | Deletes a file while retaining its contents in the undo log. |
|
||
| `rmdir` | Recursively deletes a directory. Its undo entry records the path but not the removed contents. |
|
||
| `bash_exec` | Runs `bash -c` with the workspace as its initial working directory. Requires explicit approval, closes stdin, disables interactive/color behavior, limits output, and kills the process tree at timeout. |
|
||
| `websearch` | Uses Tavily to return a synthesized answer and ranked web results. It fails when no Tavily key is configured. |
|
||
| `browse_url` | Uses Tavily to extract one or more web pages, optionally focused by a relevance query. It fails when no Tavily key is configured. |
|
||
| `ask_user` | Prompts the user for clarification or a decision, with up to three suggested choices and a free-text option. |
|
||
| `ask_an_expert` | Displays a copyable question for a human expert and waits for the user to paste the answer back. It does not contact an external expert directly. |
|
||
|
||
The web tools remain registered even when `TAVILY_API_KEY` is absent; configuration is validated lazily when one is invoked.
|
||
|
||
## Built-In Commands
|
||
|
||
| Command | Behavior |
|
||
| --- | --- |
|
||
| `/paste` | Temporarily hands the terminal to `$EDITOR` for multiline input, then sends the resulting text to the model. |
|
||
| `/suspend [description]` | Saves the current message array as a timestamped JSON snapshot. A description is requested if omitted. |
|
||
| `/recall` | Lets the user select a saved snapshot. The old conversation is restored under a newly built system prompt. |
|
||
| `/undo` | Displays the most recent mutation-log entries. It currently inspects the log; it does not execute a rollback operation. |
|
||
| `/reasoning [level]` | Shows or sets provider reasoning effort to `minimal`, `low`, `medium`, or `high`. Omitting the level leaves the model default in effect. |
|
||
| `/quit` | Ends the session. |
|
||
|
||
Typing `/` in the main input opens inline command completion. All recognized slash commands are handled locally and do not consume a model turn.
|
||
|
||
## Terminal UI
|
||
|
||
The UI is a React application rendered by Ink in the terminal's alternate screen.
|
||
|
||
- **Header:** model, workspace, reasoning setting, and history-scroll state.
|
||
- **History:** bottom-anchored transcript with streaming reasoning and line-granular scrolling.
|
||
- **Command bar:** main input, local command completion, approval prompts, selections, confirmations, and the `Ctrl+O` file browser.
|
||
- **Footer:** cumulative model calls, input/output/cache tokens, estimated cost, and key hints.
|
||
- **History controls:** `PgUp`/`PgDn`, mouse wheel, and `End` to return to the live tail.
|
||
- **Cancellation:** `Ctrl+C` at the main prompt exits; during a model turn it aborts the provider request and restores the draft.
|
||
|
||
The store retains the latest 1,000 logical history entries, while the view mounts only the lines that fit in the current terminal. Mouse reporting can be disabled with `FIDO_NO_MOUSE=1`.
|
||
|
||
### Connection States
|
||
|
||
While a turn runs, the command bar shows an animated spinner plus one of five states. The state is a closed enum (`src/turn-state.js`) and every transition is driven by an observable provider event rather than a timer, so the label always names what the stream is currently doing.
|
||
|
||
| State | Label | Entered when |
|
||
| --- | --- | --- |
|
||
| `idle` | *(the `› (idle)` placeholder)* | Session start, after a turn completes, after cancel, after an error. |
|
||
| `connecting` | `connecting…` | The turn started and the request is in flight, but no delta has arrived yet. |
|
||
| `thinking` | `thinking…` | Every reasoning delta. |
|
||
| `working` | `working…` | Every tool-call delta. |
|
||
| `streaming` | `streaming…` | Every content delta. |
|
||
|
||
A turn therefore reads as `connecting → streaming` for a plain answer, `connecting → thinking → streaming` when the model reasons first, and `connecting → thinking → working → streaming` when it also calls a tool. Each delta wins immediately, so a later reasoning delta can move the label back from `streaming` to `thinking`; the label tracks the newest activity rather than the furthest point reached. Cancel and error are terminal: both fall back to `idle` so the prompt always returns.
|
||
|
||
The REPL (`src/repl.js`) is the only module that decides a transition, and it does so through a pure `(state, event) -> state` reducer, so the machine is unit-testable without React, the store, or a live provider. Text supplied by the provider's `onStatus` is treated as a non-authoritative detail shown next to the state label, never as a replacement for it.
|
||
|
||
## File Mentions
|
||
|
||
Press `Ctrl+O` in the main prompt to open the file browser.
|
||
|
||
- Arrow keys move the selection.
|
||
- `Enter` opens a directory or enters a file's line selector.
|
||
- `Tab` inserts a whole-file mention from the browser.
|
||
- `Backspace` moves to the parent directory.
|
||
- `Esc` leaves an overlay.
|
||
- In line mode, `v` starts or ends a range, `g`/`G` jump to the first/last line, and `Enter` inserts the mention.
|
||
|
||
Mentions are references, not embedded file contents:
|
||
|
||
```text
|
||
@README.md
|
||
@src/fido.js:12
|
||
@src/fido.js:12-40
|
||
```
|
||
|
||
The browser hides ignored and dot-directory entries by default. Mentions use forward-slash workspace-relative paths and one-based line numbers; the model maps those numbers to `read_file`'s zero-based `seek`.
|
||
|
||
## Safety and Trust Boundaries
|
||
|
||
Fido has useful path and workflow safeguards, but it is not an operating-system sandbox.
|
||
|
||
- Dedicated filesystem tools require relative paths and reject lexical paths that resolve outside `ROOT_DIR`.
|
||
- Direct filesystem access can still follow filesystem semantics such as symlinks; path checks should not be treated as a hard kernel-enforced isolation boundary.
|
||
- `bash_exec` pins only the shell's starting directory. The shell can use absolute paths or change directories, so interactive approval is the primary protection around shell execution.
|
||
- Users can approve one command or all shell commands for the rest of that Fido session.
|
||
- Network and extracted web content is explicitly treated as untrusted third-party input.
|
||
- The model is instructed to inspect files before patching and to prefer targeted replacements over whole-file rewrites.
|
||
- API keys are loaded from environment files that are excluded by `.gitignore`; installed configuration lives under `FIDO_HOME`.
|
||
- Tool notifications shorten large arguments, but full model tool results remain part of the provider conversation.
|
||
|
||
## State and Persistence
|
||
|
||
### Installed layout
|
||
|
||
The installer defaults to `FIDO_HOME=~/.fido`:
|
||
|
||
```text
|
||
~/.fido/
|
||
app/ # this repository
|
||
llmorbiter/ # bundled local dependency
|
||
bin/fido # launcher
|
||
.env # API keys and configuration
|
||
state/
|
||
<workspace-slug>-<checksum>/
|
||
undo.json
|
||
suspends/
|
||
<timestamp>-<description>.json
|
||
```
|
||
|
||
The workspace slug is derived from the canonical workspace path and augmented with a checksum, keeping state isolated between projects and avoiding repository litter.
|
||
|
||
### Source-checkout defaults
|
||
|
||
When run without the installed launcher, the defaults are:
|
||
|
||
```text
|
||
UNDO_FILE=.fido-undo.json
|
||
SUSPEND_DIR=.fido-suspends
|
||
```
|
||
|
||
These paths are resolved from the selected workspace. They are covered by the repository's `.fido-*` ignore rule.
|
||
|
||
Normal conversations are not automatically persisted on exit. `/suspend` and `/recall` provide explicit manual snapshots.
|
||
|
||
## Configuration
|
||
|
||
Copy `.env.example` to `.env` and set at least `OPENROUTER_API_KEY`.
|
||
|
||
| Variable | Required | Default / meaning |
|
||
| --- | --- | --- |
|
||
| `OPENROUTER_API_KEY` | Yes | Credential for the OpenRouter provider. |
|
||
| `MODEL` | No | `xiaomi/mimo-v2.6-flash` |
|
||
| `TAVILY_API_KEY` | No | Enables `websearch` and `browse_url`. |
|
||
| `ROOT_DIR` | No | Workspace root when no positional path is supplied. |
|
||
| `UNDO_FILE` | No | Undo-log file; the installed launcher overrides it per workspace. |
|
||
| `SUSPEND_DIR` | No | Suspend directory; the installed launcher overrides it per workspace. |
|
||
| `COMPACT_DEBUG` | No | Set to `1` to report transcript-compaction statistics. |
|
||
| `FIDO_HOME` | No | Installer/launcher home, default `~/.fido`. |
|
||
| `LLMORBITER_SRC` | No | Installer override for the sibling `llmorbiter` checkout. |
|
||
| `BASHRC` | No | Installer shell RC file, default `~/.bashrc`. |
|
||
| `FIDO_SKIP_DEPS` | No | Set to `1` to let the installer skip `npm install`. |
|
||
| `FIDO_NO_MOUSE` | No | Set to `1` to disable terminal mouse reporting. |
|
||
|
||
For installed use, launcher-exported variables take precedence over workspace `.env` values. The source application also checks the repository's `.env` and then the current working directory's `.env`.
|
||
|
||
## Installation and Development
|
||
|
||
### Source checkout
|
||
|
||
Fido requires the sibling local dependency declared in `package.json`:
|
||
|
||
```text
|
||
<parent-directory>/
|
||
fido/
|
||
llmorbiter/
|
||
```
|
||
|
||
From the Fido checkout:
|
||
|
||
```sh
|
||
npm install
|
||
cp .env.example .env
|
||
# Edit .env and set OPENROUTER_API_KEY.
|
||
npm start
|
||
```
|
||
|
||
Run against another workspace with:
|
||
|
||
```sh
|
||
npm start -- /path/to/project
|
||
```
|
||
|
||
### Installed launcher
|
||
|
||
```sh
|
||
./install.sh
|
||
source ~/.bashrc
|
||
fido
|
||
# or
|
||
fido /path/to/project
|
||
```
|
||
|
||
The installer copies the app and `llmorbiter` into `FIDO_HOME`, installs dependencies, installs the launcher, and adds `$FIDO_HOME/bin` to `PATH`. Reinstalling preserves an existing `$FIDO_HOME/.env`. Use `./install.sh --uninstall` to remove the install and its PATH block.
|
||
|
||
If `llmorbiter` is not a sibling checkout:
|
||
|
||
```sh
|
||
LLMORBITER_SRC=/path/to/llmorbiter ./install.sh
|
||
```
|
||
|
||
### Tests
|
||
|
||
```sh
|
||
npm test
|
||
# or one file
|
||
node --test test/compact.test.js
|
||
```
|
||
|
||
The suite uses Node's built-in test runner and focuses on pure helper behavior: transcript compaction, file-browser paths and mention formatting, glob translation, line counting, mouse decoding, and UI/store rendering behavior. Unit tests do not make live model or Tavily requests.
|
||
|
||
## Extending the Project
|
||
|
||
To add a model-facing tool:
|
||
|
||
1. Create a module under `src/tools/` using `defineTool` from `llmorbiter`.
|
||
2. Validate arguments and return structured data or throw `ToolError`.
|
||
3. Use `resolveSafe` for workspace-relative filesystem paths.
|
||
4. Export a concise `notifyCall` and, when useful, a tailored `notifyResult` renderer.
|
||
5. Register the tool and notification renderers in `src/tools/index.js`.
|
||
6. Add its contract to `src/system-prompt.js` when the model needs extra usage rules.
|
||
7. Add focused tests that do not require live network or provider calls.
|
||
|
||
The non-React REPL and tool modules should interact with rendering through the `ui` facade in `src/ui/index.js`; direct console writes would bypass the full-screen UI state.
|
||
|
||
## Current Status and Planned Work
|
||
|
||
The current implementation does **not** include persistent todo plans or subagent delegation. `PLAN.md` is a proposed design for those features, including an `update_plan` tool, a right-side plan panel, a bounded read-only `delegate` tool, and separate subagent activity/status UI. Those capabilities should not be treated as available until their modules, registry entries, persistence, and tests are implemented.
|
||
|
||
Other practical limitations today include manual-only conversation snapshots, a view-only `/undo` command, no kernel-level sandbox, and no live provider integration tests in the standard suite.
|
||
|
||
## License
|
||
|
||
Fido is released under the [0BSD License](README.md#license).
|