Files
蓝莓🫐 bc41885f0b feat(stage-ui,agents,core-agent): let the model pick a Tabler icon for each tool call (#3051)
## Description

A tool call row shows the summary of the call, but every call of a tool
without its own icon shows the same magic wand. With this PR, the model
picks an icon for each call, from the Tabler icon set.

- Every tool gets a second argument `call_icon` after `call_summary`: up
to three Tabler icon names for what the call does, best first. The tool
never gets it, the same as `call_summary`.
- The chat draws the first name that exists. A name that does not exist
goes through a search of the icon names, so a close name still finds an
icon. Without a match, the row keeps the default icon of the tool.
- The picked icon replaces the `--un-icon` mask of the icon element that
the row and the pill already have, so size and color stay. There is no
extra request, and nothing is stored outside the recorded arguments.
- Cards can use any Tabler icon. The card model and the chat model for
reply cards get `builtIn_searchIcons`. It searches the words of the icon
names, and a variant, for example `bookmark-off`, goes with its icon.
The card validator rejects a name that does not exist.
- The default tool icons, the skill icons, and the error icon of the
pill are Tabler outline icons too, so one row of icons uses one set.
- A task beside the chat, for example the card model, gets only its own
tools. It does not get the tools of the chat, or the web search that
OpenAI Responses adds from the provider settings.

Why the model writes names: I compared names with local matching on two
test sets. One set has 400 English tool call labels, in four kinds. The
other has 104 labels in the 10 languages of AIRI. `gpt-6-sol` judged
each icon. The first icon from names fit 89% and 91% of the labels. The
best local method fit 74%. That method was a search of the Tabler tags
with a trained ranker, or EmbeddingGemma on the label itself. Names cost
a few tokens per call. In the 1437 labels, the names held a real icon
name 1427 times, so the search that corrects a name runs rarely.

## Changes

| Part | Change |
| --- | --- |
| `stores/ai/chat-llm/tool-call-summary.ts` | Adds `call_icon` after
`call_summary`, and removes both before the tool gets its input.
`readCallSummary` reads the line and the icon names, each with its own
fallback. |
| `libs/tabler-icons.ts` | New. Loads the Tabler icons on first use.
Draws an icon by name for cards, searches icons by the words of their
names, and turns `call_icon` candidates into the `--un-icon` mask of a
row. |
| `tools/search-icons.ts` | New. `builtIn_searchIcons` for the card
model and for reply cards. |
| `tool-call-group.vue` | The row and the pill draw the picked icon. The
error icon of the pill is Tabler. |
| `tools/icons.ts` | The default tool icons and the skill icons are
Tabler outline icons. |
| `libs/cards/catalog.ts`, `libs/cards/program.ts` | Cards draw Tabler
icons, and the validator checks the names against the Tabler set. |
| `stores/tool-cards.ts`, `stores/chat.ts` | The card model gets the
icon search. A chat that draws reply cards gets it with the card tool. |
| `composables/text-generation.ts`, `stores/ai/chat-llm/llm.ts`,
`@proj-airi/agents` | `generateText` takes the tools of a task. A
request with task tools gets no tools of the chat and no tools of the
provider. |
| `@proj-airi/core-agent` | New stream option `providerTools`. When it
is `false`, OpenAI Responses adds no web search. |
| `package.json` files | `@iconify-json/tabler` for stage-ui and for the
UnoCSS classes of the desktop, mobile, and Kirie apps. It is the only
source of the icons. |

## Verification

- The pre-commit hook linted the staged files of each commit. I did not
run the typecheck, the tests, or the full lint locally. CI runs them.
- `.vishot/capture-tool-call-icons.mjs` (local and ignored) puts one
reply with three calls into the stage-web chat. Each call has a summary
and icon names: a web search and a weather call that finished, and a
page read that failed. It opens the pill and captures at 1440×1600,
light, English. The same script ran against `main` (`c8943f47c`) and
against this branch.

## Visual changes

| Before | After |
|---|---|
|
![](https://github.com/user-attachments/assets/800e4964-7bb4-485a-94e6-0eafd295b0c6)
|
![](https://github.com/user-attachments/assets/5be33316-935d-4571-927d-5de193165c52)
|
| Chat / Tool call panel | Chat / Tool call panel |

## Additional Context

- Each tool schema in a request grows by one more property, and each
call carries up to three short names.
- The icon search matches the words of the icon names only. A search for
an idea, for example "save", finds fewer icons than a search of the
Tabler tags. The model searches again with a thing that the icon draws,
for example "device floppy". The names that a model writes in
`call_icon` almost never need the search.
- A tool card saved before this PR keeps the MingCute icon names that
its model wrote. A name that Tabler does not have draws no icon. Making
the card again fixes it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-10-20 19:43:42 +07:00
..
2026-08-26 20:13:10 +08:00