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:
dj-oyu 2026-02-24 15:43:41 +09:00
parent 7a3e6b0543
commit d167c94cdd
5 changed files with 45 additions and 31 deletions

View file

@ -1036,28 +1036,32 @@ func formatMarkdownTable(lines []string) string {
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 {
w := 0
for _, r := range s {
switch {
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++
}
w += runeDisplayWidth(r)
}
return w
}
@ -1092,7 +1096,7 @@ func wrapByDisplayWidth(s string, maxWidth int) []string {
flush()
continue
}
rw := displayWidth(string(r))
rw := runeDisplayWidth(r)
if rw == 0 {
cur.WriteRune(r)
continue

View file

@ -67,12 +67,11 @@ func SanitizeBranchName(task string) string {
s = "worktree"
}
// Truncate to 40 chars
runes := []rune(s)
if len(runes) > 40 {
runes = runes[:40]
// Truncate to 40 chars (ASCII fast path: branch names are ASCII after sanitization)
if len(s) > 40 {
s = s[:40]
}
s = strings.TrimRight(string(runes), "-")
s = strings.TrimRight(s, "-")
return "plan/" + s
}

View file

@ -130,7 +130,9 @@ func (p *ClaudeCliProvider) buildToolsPrompt(tools []ToolDefinition) string {
}
if len(tool.Function.Parameters) > 0 {
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")
}

View file

@ -278,7 +278,7 @@ func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, cou
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 {
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
bodyStr := string(body)
if strings.Contains(contentType, "application/json") {
var jsonData any
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)
extractor = "json"
} else {
text = string(body)
text = bodyStr
extractor = "raw"
}
} else if strings.Contains(contentType, "text/html") || len(body) > 0 &&
(strings.HasPrefix(string(body), "<!DOCTYPE") || strings.HasPrefix(strings.ToLower(string(body)), "<html")) {
text = t.extractText(string(body))
(strings.HasPrefix(bodyStr, "<!DOCTYPE") || strings.HasPrefix(strings.ToLower(bodyStr), "<html")) {
text = t.extractText(bodyStr)
extractor = "text"
} else {
text = string(body)
text = bodyStr
extractor = "raw"
}

View file

@ -47,6 +47,10 @@ func TailPad(s string, n, wrapWidth int) string {
// wrapLine splits a single line into segments of at most width runes.
// An empty line produces one empty string (preserving blank lines).
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)
if len(runes) <= width {
return []string{line}
@ -97,6 +101,10 @@ func Truncate(s string, maxLen int) string {
if maxLen <= 0 {
return ""
}
// ASCII fast path: byte length == rune length for pure ASCII
if len(s) <= maxLen {
return s
}
runes := []rune(s)
if len(runes) <= maxLen {
return s