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 <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-11-03 08:38:53 +00:00
1 parent dafaa33590
commit 8079414946
2 files changed
+14 -12

No files matched your search

+6 -12
View File
@@ -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 })
}
@@ -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())