perf: reduce []byte/string conversions and add ASCII fast paths
- bytes.NewReader instead of strings.NewReader(string(...)) in web search - Consolidate multiple string(body) calls into single variable - sb.Write(paramsJSON) instead of string conversion in CLI provider - ASCII fast paths in Truncate/wrapLine before []rune conversion - Byte-length truncation for ASCII-only branch names - Extract runeDisplayWidth to avoid per-rune string(r) in wrap loop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
66c5f2d897
commit
77e0862827
5 changed files with 45 additions and 31 deletions
|
|
@ -1036,28 +1036,32 @@ func formatMarkdownTable(lines []string) string {
|
||||||
return strings.TrimRight(b.String(), "\n")
|
return strings.TrimRight(b.String(), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runeDisplayWidth(r rune) int {
|
||||||
|
switch {
|
||||||
|
case r == '\u200d' || r == '\u200c' || r == '\ufe0f':
|
||||||
|
return 0
|
||||||
|
case unicode.Is(unicode.Mn, r):
|
||||||
|
return 0
|
||||||
|
case isEmojiRune(r):
|
||||||
|
return 3
|
||||||
|
case unicode.In(r,
|
||||||
|
unicode.Han,
|
||||||
|
unicode.Hiragana,
|
||||||
|
unicode.Katakana,
|
||||||
|
unicode.Hangul):
|
||||||
|
return 2
|
||||||
|
case (r >= 0x3000 && r <= 0x303F) || (r >= 0xFF00 && r <= 0xFFEF):
|
||||||
|
// CJK symbols/punctuation and half/fullwidth forms.
|
||||||
|
return 2
|
||||||
|
default:
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func displayWidth(s string) int {
|
func displayWidth(s string) int {
|
||||||
w := 0
|
w := 0
|
||||||
for _, r := range s {
|
for _, r := range s {
|
||||||
switch {
|
w += runeDisplayWidth(r)
|
||||||
case r == '\u200d' || r == '\u200c' || r == '\ufe0f':
|
|
||||||
continue
|
|
||||||
case unicode.Is(unicode.Mn, r):
|
|
||||||
continue
|
|
||||||
case isEmojiRune(r):
|
|
||||||
w += 3
|
|
||||||
case unicode.In(r,
|
|
||||||
unicode.Han,
|
|
||||||
unicode.Hiragana,
|
|
||||||
unicode.Katakana,
|
|
||||||
unicode.Hangul):
|
|
||||||
w += 2
|
|
||||||
case (r >= 0x3000 && r <= 0x303F) || (r >= 0xFF00 && r <= 0xFFEF):
|
|
||||||
// CJK symbols/punctuation and half/fullwidth forms.
|
|
||||||
w += 2
|
|
||||||
default:
|
|
||||||
w++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
@ -1092,7 +1096,7 @@ func wrapByDisplayWidth(s string, maxWidth int) []string {
|
||||||
flush()
|
flush()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rw := displayWidth(string(r))
|
rw := runeDisplayWidth(r)
|
||||||
if rw == 0 {
|
if rw == 0 {
|
||||||
cur.WriteRune(r)
|
cur.WriteRune(r)
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,11 @@ func SanitizeBranchName(task string) string {
|
||||||
s = "worktree"
|
s = "worktree"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate to 40 chars
|
// Truncate to 40 chars (ASCII fast path: branch names are ASCII after sanitization)
|
||||||
runes := []rune(s)
|
if len(s) > 40 {
|
||||||
if len(runes) > 40 {
|
s = s[:40]
|
||||||
runes = runes[:40]
|
|
||||||
}
|
}
|
||||||
s = strings.TrimRight(string(runes), "-")
|
s = strings.TrimRight(s, "-")
|
||||||
|
|
||||||
return "plan/" + s
|
return "plan/" + s
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,9 @@ func (p *ClaudeCliProvider) buildToolsPrompt(tools []ToolDefinition) string {
|
||||||
}
|
}
|
||||||
if len(tool.Function.Parameters) > 0 {
|
if len(tool.Function.Parameters) > 0 {
|
||||||
paramsJSON, _ := json.Marshal(tool.Function.Parameters)
|
paramsJSON, _ := json.Marshal(tool.Function.Parameters)
|
||||||
sb.WriteString(fmt.Sprintf("Parameters:\n```json\n%s\n```\n", string(paramsJSON)))
|
sb.WriteString("Parameters:\n```json\n")
|
||||||
|
sb.Write(paramsJSON)
|
||||||
|
sb.WriteString("\n```\n")
|
||||||
}
|
}
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, cou
|
||||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", searchURL, strings.NewReader(string(payloadBytes)))
|
req, err := http.NewRequestWithContext(ctx, "POST", searchURL, bytes.NewReader(payloadBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create request: %w", err)
|
return "", fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -534,6 +534,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||||
|
|
||||||
var text, extractor string
|
var text, extractor string
|
||||||
|
|
||||||
|
bodyStr := string(body)
|
||||||
if strings.Contains(contentType, "application/json") {
|
if strings.Contains(contentType, "application/json") {
|
||||||
var jsonData any
|
var jsonData any
|
||||||
if err := json.Unmarshal(body, &jsonData); err == nil {
|
if err := json.Unmarshal(body, &jsonData); err == nil {
|
||||||
|
|
@ -541,15 +542,15 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||||
text = string(formatted)
|
text = string(formatted)
|
||||||
extractor = "json"
|
extractor = "json"
|
||||||
} else {
|
} else {
|
||||||
text = string(body)
|
text = bodyStr
|
||||||
extractor = "raw"
|
extractor = "raw"
|
||||||
}
|
}
|
||||||
} else if strings.Contains(contentType, "text/html") || len(body) > 0 &&
|
} else if strings.Contains(contentType, "text/html") || len(body) > 0 &&
|
||||||
(strings.HasPrefix(string(body), "<!DOCTYPE") || strings.HasPrefix(strings.ToLower(string(body)), "<html")) {
|
(strings.HasPrefix(bodyStr, "<!DOCTYPE") || strings.HasPrefix(strings.ToLower(bodyStr), "<html")) {
|
||||||
text = t.extractText(string(body))
|
text = t.extractText(bodyStr)
|
||||||
extractor = "text"
|
extractor = "text"
|
||||||
} else {
|
} else {
|
||||||
text = string(body)
|
text = bodyStr
|
||||||
extractor = "raw"
|
extractor = "raw"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ func TailPad(s string, n, wrapWidth int) string {
|
||||||
// wrapLine splits a single line into segments of at most width runes.
|
// wrapLine splits a single line into segments of at most width runes.
|
||||||
// An empty line produces one empty string (preserving blank lines).
|
// An empty line produces one empty string (preserving blank lines).
|
||||||
func wrapLine(line string, width int) []string {
|
func wrapLine(line string, width int) []string {
|
||||||
|
// ASCII fast path: byte length == rune length for pure ASCII
|
||||||
|
if len(line) <= width {
|
||||||
|
return []string{line}
|
||||||
|
}
|
||||||
runes := []rune(line)
|
runes := []rune(line)
|
||||||
if len(runes) <= width {
|
if len(runes) <= width {
|
||||||
return []string{line}
|
return []string{line}
|
||||||
|
|
@ -97,6 +101,10 @@ func Truncate(s string, maxLen int) string {
|
||||||
if maxLen <= 0 {
|
if maxLen <= 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
// ASCII fast path: byte length == rune length for pure ASCII
|
||||||
|
if len(s) <= maxLen {
|
||||||
|
return s
|
||||||
|
}
|
||||||
runes := []rune(s)
|
runes := []rune(s)
|
||||||
if len(runes) <= maxLen {
|
if len(runes) <= maxLen {
|
||||||
return s
|
return s
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue