File tagging — annotate project files and inject them into a chat
Overview
A user can attach tags to files inside a project. When a chat is sent, every tagged file is included in the upstream request as a synthetic message carrying the file's content (or a path/line-range excerpt) and the tags that pulled it in. Tags live in <projectDir>/.mouaif.json so the user can commit them with the project and edit them by hand. The picker is mobile-first: a list of files, a per-file tag chip strip, and an "Include in chat" toggle.
Usage
Tagging a file
From the project card's options menu or Settings → This project → File tags, the user opens File tags and sees every scanned project file. Each row shows the file name prominently and its project-relative folder underneath; root files show Project root. Binary files are labeled and cannot be newly tagged.
Tags — a free-form chip list. New tags are created by typing and pressing
,orEnter. Removing a chip is a tap on the chip's×.Excerpt — the user can pin a specific line range (start/end inclusive) instead of including the whole file. Empty excerpt means "whole file."
Include in chat — a single switch, on by default. Off means the file stays tagged but is not auto-injected into new messages; it can still be referenced explicitly with
@path/to/filein the composer.Tags are project-scoped. A tag named
apionsrc/api/users.jsis a different annotation from a tag namedapionfrontend/api.js; the chat injects both, each with its own context.
In a chat
When the user sends a message, the server pre-appends the tagged files to the upstream messages array, before the user's history. The shape of each injected message is:
{
role: 'system', // or 'user' if the user explicitly @-referenced the file
content:
'# File: src/api/users.js\n' +
'# Tags: api, auth\n' +
'# Excerpt: 1-120\n' +
'\n' +
'<file contents or excerpt>'
}
The leading header block lets the model reason about provenance. The role is system by default (the tagged files are project context); when the user types @src/api/users.js in the composer, the role is user so the model treats it as a direct reference.
A bare basename mention — @users.js instead of the full path — is also accepted. parseReferences resolves it against the tagged map first and the on-disk scan second, so @users.js promotes src/api/users.js when that basename is unambiguous (an ambiguous basename resolves to the shortest path rather than being dropped, and a mention with no match at all is silently ignored).
Behavior
Storage: per-project
.mouaif.json. New top-level keytags, shaped exactly like the GET response above. The file is rewritten on every PUT; the scan and DELETE endpoints do not touch the map.Path normalization. Paths are stored as POSIX-style relative paths from the project root (
src/api/users.js, neversrc\\api\\users.js). The chat loader resolves them withpath.join(projectDir, rel)and refuses anything that escapes the project root (..segments or absolute paths yieldEOUTSIDE_PROJECT).Excerpt format.
{ start, end }are 1-indexed inclusive line numbers. An absent excerpt means the whole file;{ start: 2, end: 2 }injects line 2.File size cap. Files above a soft cap (default 256 KB, configurable via
app.fileTagMaxBytes) are not auto-injected; the tagged entry stays in the project file withincludeInChat: falseand anote: "exceeds fileTagMaxBytes". The user can pin a smaller excerpt to bypass the cap.Stale path handling. If a tagged file is moved, renamed, or deleted, the entry is kept (the user may be in the middle of a refactor) but is silently skipped at injection time. The UI shows a
missingbadge on stale entries and offers a "Remove" action.Commit-friendliness. Because the tag map lives in
.mouaif.json, the user cangit addit next to the source. The chat injects the current working-tree version of each file at message-send time, not whatever was committed.No tags on a missing project. Tag CRUD requires the project to be registered; a PUT against an unregistered project returns
404.Binary files are out. The scan filters by extension (configurable; defaults to a text-friendly list:
.js,.jsx,.ts,.tsx,.mjs,.cjs,.json,.json5,.md,.txt,.py,.rb,.go,.rs,.java,.kt,.swift,.c,.h,.cpp,.hpp,.css,.scss,.less,.html,.vue,.svelte,.astro,.yml,.yaml,.toml,.sh,.sql,.graphql,.gql,.proto,.ini,.conf,.cfg,.env,.lock,.map,.log,.lua,.pl,.pm,.r,.jl,.dart,.zig,.ex,.exs,.erl,.hrl,.fs,.fsx,.ml,.clj,.cljs,.scala,.groovy,.gradle,.tf,.hcl,.pug,.jade,.ejs,.hbs,.mustache,.tpl,.gitignore,.gitattributes,.editorconfig,.eslintrc,.prettierrc,.babelrc,.npmrc,.nvmrc,.yarnrc,.dockerignore,.gitmodules,.htaccess,.dockerfile,.makefile). The check is against the file's extension, never the whole name, so dotted base names (App.vue,webpack.config.js) are never mistaken for extensionless binaries; extensionless build files (Makefile) are classified via the allowlist too. Unknown extensions show up in the scan asbinary: trueand cannot be tagged.
Related
docs/features/folder-picker.md — the same home-allowlist rules.
docs/features/custom-prompts.md — tags and prompts compose: a tagged file can sit under a custom system prompt in the same upstream array.
docs/features/chat-ui.md — composer and message rendering.