Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
170 lines
4.1 KiB
Go
170 lines
4.1 KiB
Go
package agenthost
|
|
|
|
import (
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
canonical "github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
|
|
)
|
|
|
|
const MaxSessionTitleRunes = 120
|
|
|
|
// NormalizeTitle converts provider/Tutti rich-text serialization to a plain
|
|
// title. It intentionally does not apply UI-local labels or localization.
|
|
// Call it only at a rich-text-to-title boundary; persisted canonical titles
|
|
// must flow through the rest of the system without being parsed again.
|
|
func NormalizeTitle(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
|
|
var output strings.Builder
|
|
for index := 0; index < len(value); {
|
|
labelStart, hrefStart, hrefEnd, ok := markdownLinkAt(value, index)
|
|
if !ok {
|
|
output.WriteByte(value[index])
|
|
index++
|
|
continue
|
|
}
|
|
output.WriteString(unescapeMarkdownLabel(value[labelStart : hrefStart-2]))
|
|
index = hrefEnd + 1
|
|
}
|
|
return strings.Join(strings.Fields(strings.TrimSpace(output.String())), " ")
|
|
}
|
|
|
|
// DeriveInitialTitle converts the first user-visible prompt into the canonical
|
|
// title for a session that still has no title. The empty result is a safe
|
|
// compare-and-set candidate: an established title is never overwritten.
|
|
func DeriveInitialTitle(currentTitle string, visiblePrompt string) string {
|
|
if strings.TrimSpace(currentTitle) != "" {
|
|
return ""
|
|
}
|
|
title := NormalizeTitle(visiblePrompt)
|
|
if title == "" {
|
|
return ""
|
|
}
|
|
if utf8.RuneCountInString(title) <= MaxSessionTitleRunes {
|
|
return title
|
|
}
|
|
const suffix = "..."
|
|
runes := []rune(title)
|
|
suffixRunes := utf8.RuneCountInString(suffix)
|
|
return strings.TrimSpace(string(runes[:MaxSessionTitleRunes-suffixRunes])) + suffix
|
|
}
|
|
|
|
// IsLegacyTitlePlaceholder recognizes historical provider/target identity titles.
|
|
// This is migration-only compatibility; live submit code must not use it.
|
|
func IsLegacyTitlePlaceholder(value string, provider string, targetAliases ...string) bool {
|
|
title := normalizeIdentity(value)
|
|
provider = normalizeIdentity(provider)
|
|
if title == "" || title == provider {
|
|
return true
|
|
}
|
|
for _, candidate := range targetAliases {
|
|
if title == normalizeIdentity(candidate) {
|
|
return true
|
|
}
|
|
}
|
|
descriptor, ok := canonical.FindProviderIdentity(provider)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, candidate := range append(
|
|
[]string{descriptor.ID, descriptor.DisplayName},
|
|
descriptor.Aliases...,
|
|
) {
|
|
if title == normalizeIdentity(candidate) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func normalizeIdentity(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func markdownLinkAt(value string, start int) (labelStart, hrefStart, hrefEnd int, ok bool) {
|
|
if start >= len(value) || value[start] != '[' {
|
|
return 0, 0, 0, false
|
|
}
|
|
labelEnd := findUnescaped(value, start+1, ']')
|
|
if labelEnd < 0 || labelEnd+1 >= len(value) || value[labelEnd+1] != '(' {
|
|
return 0, 0, 0, false
|
|
}
|
|
hrefEnd = findBalancedHrefEnd(value, labelEnd+2)
|
|
if hrefEnd < 0 {
|
|
return 0, 0, 0, false
|
|
}
|
|
return start + 1, labelEnd + 2, hrefEnd, true
|
|
}
|
|
|
|
func findUnescaped(value string, start int, target byte) int {
|
|
escaped := false
|
|
for index := start; index < len(value); index++ {
|
|
if escaped {
|
|
escaped = false
|
|
continue
|
|
}
|
|
if value[index] == '\\' {
|
|
escaped = true
|
|
continue
|
|
}
|
|
if value[index] == target {
|
|
return index
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func findBalancedHrefEnd(value string, start int) int {
|
|
depth := 0
|
|
escaped := false
|
|
for index := start; index < len(value); index++ {
|
|
if escaped {
|
|
escaped = false
|
|
continue
|
|
}
|
|
switch value[index] {
|
|
case '\\':
|
|
escaped = true
|
|
case '(':
|
|
depth++
|
|
case ')':
|
|
if depth == 0 {
|
|
return index
|
|
}
|
|
depth--
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func unescapeMarkdownLabel(value string) string {
|
|
var output strings.Builder
|
|
escaped := false
|
|
for index := 0; index < len(value); index++ {
|
|
char := value[index]
|
|
if escaped {
|
|
if strings.ContainsRune(`\\[]()`, rune(char)) {
|
|
output.WriteByte(char)
|
|
} else {
|
|
output.WriteByte('\\')
|
|
output.WriteByte(char)
|
|
}
|
|
escaped = false
|
|
continue
|
|
}
|
|
if char == '\\' {
|
|
escaped = true
|
|
continue
|
|
}
|
|
output.WriteByte(char)
|
|
}
|
|
if escaped {
|
|
output.WriteByte('\\')
|
|
}
|
|
return output.String()
|
|
}
|