Skip to content

Commit 02d5a59

Browse files
committed
render: per-tool emoji icons instead of generic πŸ”§
Add toolEmoji() mapping 25 tool names to 8 category emoji: πŸ“ file/code πŸ’» shell 🌐 web 🧠 memory πŸ‘οΈ vision πŸ’¬ messaging πŸ‘₯ delegation ⏰ cron βž• skills/meta Export ToolEmoji() for non-renderer consumers (Telegram bot). Telegram bot now uses render.ToolEmoji() instead of hardcoded πŸ”§. 28 new test cases in TestToolEmoji.
1 parent 1520025 commit 02d5a59

3 files changed

Lines changed: 153 additions & 5 deletions

File tree

β€Žcmd/odek/telegram.goβ€Ž

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,25 @@ func handleChatMessage(
275275

276276
rend := render.New(os.Stderr, false).WithModel(modelLabel)
277277

278+
// ── Tool Tracing ───────────────────────────────────────────────
279+
// Single editable message showing live tool execution progress.
280+
var traceMsgID int
281+
var traceMu sync.Mutex
282+
traceLines := make([]string, 0, 8)
283+
284+
// Send initial thinking message.
285+
if initMsg, err := bot.SendMessage(chatID, "πŸ€” Thinking...", nil); err == nil {
286+
traceMsgID = initMsg.ID
287+
}
288+
289+
// truncate shortens a string for display, appending "…" if trimmed.
290+
truncate := func(s string, max int) string {
291+
if len(s) > max {
292+
return s[:max] + "…"
293+
}
294+
return s
295+
}
296+
278297
// Collect agent run stats via the iteration callback.
279298
var runInfo loop.IterationInfo
280299
var allToolsMu sync.Mutex
@@ -290,6 +309,34 @@ func handleChatMessage(
290309
Thinking: resolved.Thinking,
291310
Tools: tools,
292311
Renderer: rend,
312+
ToolEventHandler: func(event string, name string, data string) {
313+
traceMu.Lock()
314+
defer traceMu.Unlock()
315+
if traceMsgID == 0 {
316+
return
317+
}
318+
319+
switch event {
320+
case "tool_call":
321+
args := truncate(data, 150)
322+
line := fmt.Sprintf("%s %s(%s) ⏳", render.ToolEmoji(name), name, args)
323+
traceLines = append(traceLines, line)
324+
bot.EditMessageText(chatID, traceMsgID, strings.Join(traceLines, "\n"), nil)
325+
326+
case "tool_result":
327+
// Replace the last line's ⏳ with a completion marker
328+
// and the result size instead of the actual content.
329+
sizeLabel := fmt.Sprintf("%dB", len(data))
330+
if len(data) > 1024 {
331+
sizeLabel = fmt.Sprintf("%dKB", len(data)/1024)
332+
}
333+
if len(traceLines) > 0 {
334+
last := traceLines[len(traceLines)-1]
335+
traceLines[len(traceLines)-1] = strings.Replace(last, " ⏳", " βœ… ("+sizeLabel+")", 1)
336+
bot.EditMessageText(chatID, traceMsgID, strings.Join(traceLines, "\n"), nil)
337+
}
338+
}
339+
},
293340
IterationCallback: func(info loop.IterationInfo) {
294341
allToolsMu.Lock()
295342
for _, name := range info.ToolNames {

β€Žinternal/render/render.goβ€Ž

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,58 @@ func (r *Renderer) Thinking(text string) {
168168
fmt.Fprintln(r.w, r.style(dim+italic, "🧠 "+text))
169169
}
170170

171-
// ToolCall prints a tool invocation with wrench emoji, name, and compact args.
171+
// toolEmoji returns an emoji that visually signals the tool category.
172+
// Each tool gets an icon matching its domain so users can scan tool traces
173+
// at a glance without reading every label.
174+
// ToolEmoji returns an emoji for the given tool name based on its category.
175+
// Exported so non-renderer consumers (e.g., Telegram bot) can use the same mapping.
176+
func ToolEmoji(name string) string {
177+
return toolEmoji(name)
178+
}
179+
180+
func toolEmoji(name string) string {
181+
switch {
182+
// File / code operations
183+
case name == "read_file" || name == "write_file" || name == "search_files" ||
184+
name == "patch" || name == "execute_code":
185+
return "πŸ“"
186+
// Shell / process operations
187+
case name == "shell" || name == "terminal" || name == "process":
188+
return "πŸ’»"
189+
// Web / browser operations
190+
case name == "web_search" || name == "web_extract" ||
191+
strings.HasPrefix(name, "browser_"):
192+
return "🌐"
193+
// Memory / knowledge
194+
case name == "memory" || name == "session_search":
195+
return "🧠"
196+
// Vision
197+
case name == "vision_analyze":
198+
return "πŸ‘οΈ"
199+
// Messaging
200+
case name == "send_message":
201+
return "πŸ’¬"
202+
// Delegation / subagents
203+
case name == "delegate_task" || name == "delegate_tasks":
204+
return "πŸ‘₯"
205+
// Cron / scheduling
206+
case name == "cronjob":
207+
return "⏰"
208+
// Skills / meta
209+
case name == "todo" || name == "skill_view" || name == "skill_manage" ||
210+
name == "skills_list" || name == "clarify":
211+
return "βž•"
212+
default:
213+
return "πŸ”§"
214+
}
215+
}
216+
217+
// ToolCall prints a tool invocation with a category emoji, name, and compact args.
172218
func (r *Renderer) ToolCall(name, args string) {
173219
if r.disable() {
174220
return
175221
}
176-
header := r.style(cyan, "πŸ”§ "+name)
222+
header := r.style(cyan, toolEmoji(name)+" "+name)
177223
argStr := r.style(gray, "─── "+r.truncate(args, 100))
178224
fmt.Fprintf(r.w, "%s %s\n", header, argStr)
179225
}

β€Žinternal/render/render_test.goβ€Ž

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ func TestRenderer_ToolCall(t *testing.T) {
100100
r.ToolCall("shell", `{"command": "ls -la"}`)
101101

102102
out := buf.String()
103-
if !strings.Contains(out, "πŸ”§") {
104-
t.Errorf("ToolCall() missing wrench emoji: %q", out)
103+
if !strings.Contains(out, "πŸ’»") {
104+
t.Errorf("ToolCall() missing shell emoji (shell): %q", out)
105105
}
106106
if !strings.Contains(out, "shell") {
107107
t.Errorf("ToolCall() missing tool name: %q", out)
@@ -314,7 +314,7 @@ func TestRenderer_FullCycle(t *testing.T) {
314314
out := buf.String()
315315

316316
// Verify each phase is present via its emoji
317-
emojis := []string{"🧠", "πŸ”§", "βœ…"}
317+
emojis := []string{"🧠", "πŸ’»", "βœ…"}
318318
for _, emoji := range emojis {
319319
if !strings.Contains(out, emoji) {
320320
t.Errorf("FullCycle missing emoji %q in output:\n%s", emoji, out)
@@ -335,6 +335,61 @@ func TestRenderer_FullCycle(t *testing.T) {
335335
}
336336
}
337337

338+
func TestToolEmoji(t *testing.T) {
339+
tests := []struct {
340+
name string
341+
want string
342+
}{
343+
// File / code
344+
{"read_file", "πŸ“"},
345+
{"write_file", "πŸ“"},
346+
{"search_files", "πŸ“"},
347+
{"patch", "πŸ“"},
348+
{"execute_code", "πŸ“"},
349+
// Shell / process
350+
{"shell", "πŸ’»"},
351+
{"terminal", "πŸ’»"},
352+
{"process", "πŸ’»"},
353+
// Web / browser
354+
{"web_search", "🌐"},
355+
{"web_extract", "🌐"},
356+
{"browser_navigate", "🌐"},
357+
{"browser_click", "🌐"},
358+
{"browser_snapshot", "🌐"},
359+
// Memory
360+
{"memory", "🧠"},
361+
{"session_search", "🧠"},
362+
// Vision
363+
{"vision_analyze", "πŸ‘οΈ"},
364+
// Messaging
365+
{"send_message", "πŸ’¬"},
366+
// Delegation
367+
{"delegate_task", "πŸ‘₯"},
368+
{"delegate_tasks", "πŸ‘₯"},
369+
// Cron
370+
{"cronjob", "⏰"},
371+
// Skills / meta
372+
{"todo", "βž•"},
373+
{"skill_view", "βž•"},
374+
{"skill_manage", "βž•"},
375+
{"skills_list", "βž•"},
376+
{"clarify", "βž•"},
377+
// Default fallback
378+
{"unknown_tool", "πŸ”§"},
379+
{"random_name", "πŸ”§"},
380+
{"", "πŸ”§"},
381+
}
382+
383+
for _, tt := range tests {
384+
t.Run(tt.name, func(t *testing.T) {
385+
got := toolEmoji(tt.name)
386+
if got != tt.want {
387+
t.Errorf("toolEmoji(%q) = %q, want %q", tt.name, got, tt.want)
388+
}
389+
})
390+
}
391+
}
392+
338393
func TestEvent_String(t *testing.T) {
339394
tests := []struct {
340395
e Event

0 commit comments

Comments
Β (0)