AI Chat
Overview
Finapp ships a local AI assistant powered by Ollama and @tanstack/ai. The chat lives in a slide-over panel (AiChatPanel.vue) and exposes ~24 tools that wrap Pinia store mutations and queries. No data leaves the user's machine.
Entry points
- FAB button (
AiFab.vue) - floating action button on all authenticated pages - Keyboard shortcut
Cmd/Ctrl+I(seeshortcuts.ts) - Slide-over panel - right side,
sm:max-w-md
Architecture
User input → AiChatPanel
├── useAiChat.send(text)
│ ├── pruneContext(messages) - sliding window + tool result compression
│ ├── chat() via @tanstack/ai with Ollama adapter
│ ├── streams TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END / TEXT_MESSAGE_CONTENT
│ ├── autoRetry - detects failed mutations + incomplete chains
│ ├── client-side fallback - directly executes mutations if model stalls
│ ├── toast notifications - visual feedback for mutations
│ └── lastAction tracker - enables undo
├── AiToolCallsList.vue - renders rich cards per tool
└── AiMessageContent.vue - markdown assistant text
Tool catalog
All tools live in app/components/ai/tools/ and are registered via createAllTools().
Transactions
create_trn- new expense or incomecreate_adjustment- set wallet balance to target value (delta is computed)create_transfer- transfer between wallets (supports currency exchange)update_trn- patch fields on existing transaction by iddelete_trn- delete by idduplicate_trn- clone a transaction with current datebulk_delete_trns- delete many by filter (safety cap viamaxDelete)list_trns- filter + aggregatesearch_trns- full-text search in descriptionsundo_last_action- reverse last AI mutation
Wallets
create_wallet,update_wallet,delete_wallet,list_walletsarchive_wallet/unarchive_walletset_wallet_exclude_in_totalreorder_wallets
Categories
create_category,update_category,delete_category,list_categories
Analytics
get_summary- totals, income/expense, top N categoriesget_wallet_stats- single-wallet income/expense/netcompare_periods- current vs previous period, delta and %
Settings
set_base_currency,set_locale,get_settingsset_theme- primary/neutral color, border radius, black-as-primary
System prompt
Built in useSystemPrompt.ts. Key rules:
- Tool-first: every factual question about wallets/categories/transactions MUST call a tool, not answer from memory
- Multi-turn: if required fields are missing (e.g. user said "купил кофе" without amount), ask ONE specific follow-up; merge on next turn
- Chain pattern:
delete_trn/update_trn/duplicate_trnrequirelist_trnsfirst to get a real id - FORBIDDEN: inventing ids, asking user for ids, reusing past tool results
- Mutations: rules 12a-12i cover transfer, adjustment, settings, wallet management, ordering, undo
- DATE RULE: omit
dateunless user explicitly said one - tools default to now
Context pruning
pruneContext.ts implements a sliding window + tool result compression:
maxContextTurns(default 6) - only the last N user+assistant pairs go to the modelcompressToolsAfterTurns(default 1) - older assistant messages keep tool_calls compressed to[tool_name: summary]strings
This prevents model "I already did X" hallucinations that appear in long sessions.
Auto-retry + client fallback
useAiChat.ts implements two layers of robustness:
- autoRetry - if tool call failed (validation error or
ok: false), send a retry hint with the real id fromlist_trns - executeFallback - if model still stalls after retry on
duplicate_trn/delete_trn/undo_last_action, the client directly executes the mutation and injects the result as a synthetic tool_call. This bypasses model reliability issues.
Idle clear
idleMinutesBeforeClear (default 30) - if the chat was idle for N minutes since last message, history is silently cleared on next send(). Prevents context pollution across sessions.
Model recommendations
| Model | Speed | Tools | Notes |
|---|---|---|---|
| gemma4:latest | fast | excellent | Default. Best speed/accuracy balance |
| gemma4:26b | medium | good | Use if RAM-rich and want higher accuracy |
| qwen3:8b | fast | excellent | Alternative with similar characteristics |
See modelMeta.ts. Other Ollama models can be used but are not validated.
Customization
All tunable settings live in aiStore.settings:
model,baseUrl,think,autoRetry,debugmaxContextTurns,compressToolsAfterTurns,idleMinutesBeforeClear
Accessible via the settings popover in the chat header.
Known limitations
- Multi-turn
create_trnis model-dependent. Ongemma4:latestend-to-end follow-up collection (e.g. user says "купил кофе", assistant asks for amount/wallet, assistant callscreate_trnafter answers) succeeds in roughly 70% of attempts. The model sometimes mis-maps the answer to the wrong parameter (e.g. puts "наличными" intodescriptioninstead ofwallet) or omitstype. For reliable multi-turn, preferqwen3:8bor provide all fields in one message. - Chain tools (
duplicate_trn,delete_trn,update_trn) can stall ongemma4:latestin long sessions - the model sometimes replies in text asking for an id instead of chaininglist_trns→ mutation. This is mitigated by client-side fallback (executeFallbackinuseAiChat.ts) which directly executes the mutation and injects the result as a synthetic tool_call. - "I already did that" hallucinations can still appear if
maxContextTurnsis set too high. The defaultcompressToolsAfterTurns: 1compresses old tool results to prevent this, but raising the window beyond 8-10 turns brings the risk back. - Local network dependency. If Ollama is not running at
baseUrl, the chat is unusable. There is no remote fallback by design - all inference stays local. - Validation errors from
@tanstack/aiare reported asstate: 'done'(noterror) with{ error: "..." }in the result.autoRetryhandles this via theisTcFailedhelper that inspects the result JSON. - No bulk reorder of categories -
useCategoriesStoredoes not expose asaveCategoriesOrderhelper, so the AI cannot reorder categories.
Adding a new tool
- Create
app/components/ai/tools/<name>.tsexporting acreate*Tools()function - Use
toolDefinition({ description, inputSchema, name })from@tanstack/ai - Return the full result shape you want to render (include
ok: trueon success) - Register in
tools/index.ts - Add a rich card in
AiToolCallsList.richKind()if needed - Update
useSystemPrompt.tswith a rule describing when to use the tool - Add a bench case in
/tmp/ai-bench.mjsfor regression testing