From 807941494680d0cb9044d99bf71be30e74e59607 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 3 Nov 2026 08:23:54 +0000 Subject: [PATCH] fix(studio): cut sub-agent titles on a rune boundary The title of a sub-agent chat was cut at a byte count and could end in the middle of a UTF-8 character. Use the rune-safe CapText. Also use the slices package for the tool lookup and the waiter removal. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto --- core/services/studio/subagent.go | 18 ++++++------------ core/services/studio/subagent_internal_test.go | 8 ++++++++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/core/services/studio/subagent.go b/core/services/studio/subagent.go index 5dfad2f788..7019cf478f 100644 --- a/core/services/studio/subagent.go +++ b/core/services/studio/subagent.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "strings" "github.com/mudler/LocalAI/core/services/studio/agents" @@ -540,11 +541,9 @@ func (s *Service) forgetWaiter(k string, ch <-chan store.TurnStatus) { s.mu.Lock() defer s.mu.Unlock() ws := s.waiters[k] - for i, w := range ws { - if (<-chan store.TurnStatus)(w) == ch { - s.waiters[k] = append(ws[:i:i], ws[i+1:]...) - break - } + if i := slices.IndexFunc(ws, func(w chan store.TurnStatus) bool { return (<-chan store.TurnStatus)(w) == ch }); i >= 0 { + // A copy: notifyWaiters may hold the old slice. + s.waiters[k] = slices.Delete(slices.Clone(ws), i, i+1) } if len(s.waiters[k]) == 0 { delete(s.waiters, k) @@ -643,15 +642,10 @@ func truncate(s string, n int) string { if len(s) <= n { return s } - return s[:n] + "…" + return session.CapText(s, n) + "…" } // hasTool says whether the list has a tool of that name. func hasTool(tools []session.Tool, name string) bool { - for _, t := range tools { - if t.Name == name { - return true - } - } - return false + return slices.ContainsFunc(tools, func(t session.Tool) bool { return t.Name == name }) } diff --git a/core/services/studio/subagent_internal_test.go b/core/services/studio/subagent_internal_test.go index fc503bb69d..76702c4fe4 100644 --- a/core/services/studio/subagent_internal_test.go +++ b/core/services/studio/subagent_internal_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "unicode/utf8" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -17,6 +18,13 @@ import ( // A long sub-agent run has more entries than one page; its answer is the last // assistant message, not the first page's. var _ = Describe("subagent internal", func() { + It("cuts a title on a rune boundary", func() { + got := truncate("héllo wörld", 2) + Expect(utf8.ValidString(got)).To(BeTrue(), "the title %q is cut inside a rune", got) + Expect(got).To(Equal("h…")) + Expect(truncate(" a b ", 10)).To(Equal("a b")) + }) + It("final text of a long child is its last answer", func() { st, err := store.NewJSON("") Expect(err).NotTo(HaveOccurred())