Say what a tool did, not where it ran
An agent event set its label to the SSH profile's name, so the transcript read "homeserver · ls -la" -- naming the machine rather than the thing that was done. Built-in tools set no label at all and fell back to the function name, so a saved memory read "memory_add". The status line said "Running shell_run…" and the approval card had its own hand-written wording. Four places, four answers, nothing checking that any of them agreed. services/tool_labels.py is the one table all of them read now. Bash, Read, Write, List, Web search, Memory saved; an icon each, instead of everything being the sparkle. The precedence is inverted on purpose. Tool events are persisted in Message.tool_calls_json, so every agent row already on disk carries the profile name -- a resolver that preferred the stored value would fix nothing for any transcript that already exists. So a name the table knows resolves from the table, and a name it does not -- a custom HTTP tool, an MCP tool, whose labels are per row and cannot be tabulated -- keeps its own. One rule, both cases correct. The machine moves to `detail`, where "where this ran" belongs. tool_label and tool_icon are Jinja globals because a message bubble is rendered from four handlers, and a fifth thing each of them must remember to pass is a fifth thing one of them will forget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,23 +19,24 @@
|
||||
`kind` says how to label the event. Rows written before it existed have none,
|
||||
so web_search reads as a search and everything else falls to the generic
|
||||
branch: an old notes_search event used to claim it had searched the web.
|
||||
|
||||
What a tool is *called* is not decided here. `tool_label` and `tool_icon` are
|
||||
Jinja globals over services/tool_labels.py, which is the one table the status
|
||||
line and the approval card read too. It deliberately ignores an `event.label`
|
||||
it recognises the name of: rows already on disk carry the SSH profile's name,
|
||||
so a resolver that preferred the stored value would leave every existing
|
||||
transcript saying "homeserver" where it means "Bash".
|
||||
#}
|
||||
{% for event in tool_events %}
|
||||
{% set kind = event.kind or ('search' if event.name == 'web_search' else 'tool') %}
|
||||
<details class="tool-activity {{ 'tool-activity--error' if event.status == 'error' }}">
|
||||
<summary class="tool-activity__summary">
|
||||
{% if kind == 'search' %}
|
||||
{{ icon("globe", "icon--sm tool-activity__icon") }}
|
||||
{% elif kind == 'custom' %}
|
||||
{{ icon("link", "icon--sm tool-activity__icon") }}
|
||||
{% elif kind == 'mcp' %}
|
||||
{{ icon("server", "icon--sm tool-activity__icon") }}
|
||||
{% else %}
|
||||
{{ icon("sparkle", "icon--sm tool-activity__icon") }}
|
||||
{% endif %}
|
||||
{{ icon(tool_icon(event), "icon--sm tool-activity__icon") }}
|
||||
|
||||
<span class="tool-activity__label">
|
||||
{% if kind == 'search' %}
|
||||
{# Prose, not "Web search · mallorn". A search is the one thing here
|
||||
common enough to be worth a sentence. #}
|
||||
{% if event.status == "error" %}
|
||||
Web search failed
|
||||
{% elif event.query %}
|
||||
@@ -44,7 +45,7 @@
|
||||
Searched the web
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set label = event.label or event.name %}
|
||||
{% set label = tool_label(event) %}
|
||||
{% if event.status == "error" %}
|
||||
{{ label }} failed
|
||||
{% else %}
|
||||
|
||||
@@ -195,6 +195,12 @@
|
||||
<path d="M13.5 3.5V9H19M8.5 13h7M8.5 16.5h7"/>
|
||||
</symbol>
|
||||
|
||||
<!-- Changing part of a file. A plus over a minus, which is what a diff looks
|
||||
like everywhere else; the pencil is already taken by writing one whole. -->
|
||||
<symbol id="i-diff" viewBox="0 0 24 24">
|
||||
<path d="M12 3.5v7M8.5 7h7M8.5 17h7"/>
|
||||
</symbol>
|
||||
|
||||
<!-- A drag handle. Dots rather than lines: lines at this size read as a
|
||||
hamburger, which means something else entirely. -->
|
||||
<symbol id="i-grip" viewBox="0 0 24 24">
|
||||
|
||||
@@ -12,6 +12,7 @@ from lembas import __version__
|
||||
from lembas.config import settings
|
||||
from lembas.db.models import User
|
||||
from lembas.services import metrics as metrics_service
|
||||
from lembas.services import tool_labels
|
||||
from lembas.services.markdown import highlight_tokens
|
||||
from lembas.services.reasoning import format_duration
|
||||
|
||||
@@ -51,6 +52,14 @@ templates.env.filters["stable_hue"] = stable_hue
|
||||
# every one of them would otherwise have to remember to pass it.
|
||||
templates.env.filters["tokens"] = highlight_tokens
|
||||
|
||||
# What a tool call is called and what it looks like. Globals rather than
|
||||
# context values because a message bubble is rendered from four different
|
||||
# handlers -- pages, post_message, regenerate and the SSE follower -- and every
|
||||
# one of them would otherwise have to remember to pass them. That is the exact
|
||||
# trap `audio_service.template_flags` fell into.
|
||||
templates.env.globals["tool_label"] = tool_labels.label_for
|
||||
templates.env.globals["tool_icon"] = tool_labels.icon_for
|
||||
|
||||
|
||||
def resolve_theme(user: User | None) -> str:
|
||||
"""Theme to render with on the server.
|
||||
|
||||
Reference in New Issue
Block a user