|
5 | 5 | "fmt" |
6 | 6 | "os" |
7 | 7 | "path/filepath" |
| 8 | + "strconv" |
8 | 9 | "strings" |
9 | 10 | "time" |
10 | 11 |
|
@@ -156,11 +157,307 @@ func (b *codexSessionBuilder) handleFunctionCall( |
156 | 157 | func formatCodexFunctionCall( |
157 | 158 | name string, payload gjson.Result, |
158 | 159 | ) string { |
159 | | - summary := payload.Get("summary").Str |
160 | | - if summary != "" { |
161 | | - return fmt.Sprintf("[%s: %s]", name, summary) |
| 160 | + summary := sanitizeToolLabel(payload.Get("summary").Str) |
| 161 | + args, rawArgs := parseCodexFunctionArgs(payload) |
| 162 | + |
| 163 | + switch name { |
| 164 | + case "exec_command", "shell_command", "shell": |
| 165 | + return formatCodexBashCall(summary, args, rawArgs) |
| 166 | + case "write_stdin": |
| 167 | + return formatCodexWriteStdinCall(summary, args, rawArgs) |
| 168 | + case "apply_patch": |
| 169 | + return formatCodexApplyPatchCall(summary, args, rawArgs) |
162 | 170 | } |
163 | | - return fmt.Sprintf("[%s]", name) |
| 171 | + |
| 172 | + category := NormalizeToolCategory(name) |
| 173 | + if category == "Other" { |
| 174 | + header := formatToolHeader("Tool", name) |
| 175 | + if summary != "" { |
| 176 | + return header + "\n" + summary |
| 177 | + } |
| 178 | + if preview := codexArgPreview(args, rawArgs); preview != "" { |
| 179 | + return header + "\n" + preview |
| 180 | + } |
| 181 | + return header |
| 182 | + } |
| 183 | + |
| 184 | + detail := firstNonEmpty(summary, |
| 185 | + codexCategoryDetail(category, args)) |
| 186 | + header := formatToolHeader(category, detail) |
| 187 | + if preview := codexArgPreview(args, rawArgs); preview != "" { |
| 188 | + return header + "\n" + preview |
| 189 | + } |
| 190 | + return header |
| 191 | +} |
| 192 | + |
| 193 | +func parseCodexFunctionArgs( |
| 194 | + payload gjson.Result, |
| 195 | +) (gjson.Result, string) { |
| 196 | + for _, key := range []string{"arguments", "input"} { |
| 197 | + arg := payload.Get(key) |
| 198 | + if !arg.Exists() { |
| 199 | + continue |
| 200 | + } |
| 201 | + |
| 202 | + switch arg.Type { |
| 203 | + case gjson.String: |
| 204 | + s := strings.TrimSpace(arg.Str) |
| 205 | + if s == "" { |
| 206 | + continue |
| 207 | + } |
| 208 | + if gjson.Valid(s) { |
| 209 | + return gjson.Parse(s), "" |
| 210 | + } |
| 211 | + return gjson.Result{}, s |
| 212 | + default: |
| 213 | + if arg.IsObject() || arg.IsArray() { |
| 214 | + return arg, "" |
| 215 | + } |
| 216 | + raw := strings.TrimSpace(arg.Raw) |
| 217 | + if raw == "" { |
| 218 | + continue |
| 219 | + } |
| 220 | + if gjson.Valid(raw) { |
| 221 | + return gjson.Parse(raw), "" |
| 222 | + } |
| 223 | + return gjson.Result{}, raw |
| 224 | + } |
| 225 | + } |
| 226 | + return gjson.Result{}, "" |
| 227 | +} |
| 228 | + |
| 229 | +func formatCodexBashCall( |
| 230 | + summary string, args gjson.Result, rawArgs string, |
| 231 | +) string { |
| 232 | + cmd := codexArgValue(args, "cmd", "command") |
| 233 | + if cmd == "" && rawArgs != "" && !gjson.Valid(rawArgs) { |
| 234 | + cmd = rawArgs |
| 235 | + } |
| 236 | + if cmd == "" && args.Type == gjson.String { |
| 237 | + cmd = strings.TrimSpace(args.Str) |
| 238 | + } |
| 239 | + |
| 240 | + header := formatToolHeader("Bash", summary) |
| 241 | + if cmd != "" { |
| 242 | + return header + "\n$ " + cmd |
| 243 | + } |
| 244 | + if preview := codexArgPreview(args, rawArgs); preview != "" { |
| 245 | + return header + "\n" + preview |
| 246 | + } |
| 247 | + return header |
| 248 | +} |
| 249 | + |
| 250 | +func formatCodexWriteStdinCall( |
| 251 | + summary string, args gjson.Result, rawArgs string, |
| 252 | +) string { |
| 253 | + if summary == "" { |
| 254 | + if sid := codexArgValue(args, "session_id"); sid != "" { |
| 255 | + summary = "stdin -> " + sid |
| 256 | + } else { |
| 257 | + summary = "stdin" |
| 258 | + } |
| 259 | + } |
| 260 | + |
| 261 | + header := formatToolHeader("Bash", summary) |
| 262 | + chars := codexArgString(args, "chars") |
| 263 | + if chars != "" { |
| 264 | + quoted := strings.Trim( |
| 265 | + strconv.QuoteToASCII(chars), "\"", |
| 266 | + ) |
| 267 | + return header + "\n" + truncate(quoted, 220) |
| 268 | + } |
| 269 | + |
| 270 | + if preview := codexArgPreview(args, rawArgs); preview != "" { |
| 271 | + return header + "\n" + preview |
| 272 | + } |
| 273 | + return header |
| 274 | +} |
| 275 | + |
| 276 | +func formatCodexApplyPatchCall( |
| 277 | + summary string, args gjson.Result, rawArgs string, |
| 278 | +) string { |
| 279 | + patch := codexArgString(args, "patch") |
| 280 | + if patch == "" && strings.Contains(rawArgs, "*** Begin Patch") { |
| 281 | + patch = rawArgs |
| 282 | + } |
| 283 | + |
| 284 | + files := extractPatchedFiles(patch) |
| 285 | + if summary == "" { |
| 286 | + summary = summarizePatchedFiles(files) |
| 287 | + } |
| 288 | + |
| 289 | + header := formatToolHeader("Edit", summary) |
| 290 | + if len(files) > 1 { |
| 291 | + limit := min(len(files), 6) |
| 292 | + body := strings.Join(files[:limit], "\n") |
| 293 | + if len(files) > limit { |
| 294 | + body += fmt.Sprintf("\n+%d more files", len(files)-limit) |
| 295 | + } |
| 296 | + return header + "\n" + body |
| 297 | + } |
| 298 | + if preview := codexArgPreview(args, rawArgs); preview != "" && |
| 299 | + len(files) == 0 { |
| 300 | + return header + "\n" + preview |
| 301 | + } |
| 302 | + return header |
| 303 | +} |
| 304 | + |
| 305 | +func extractPatchedFiles(patch string) []string { |
| 306 | + if patch == "" { |
| 307 | + return nil |
| 308 | + } |
| 309 | + |
| 310 | + var files []string |
| 311 | + seen := make(map[string]struct{}) |
| 312 | + lines := strings.Split(patch, "\n") |
| 313 | + for _, line := range lines { |
| 314 | + for _, prefix := range []string{ |
| 315 | + "*** Add File: ", |
| 316 | + "*** Update File: ", |
| 317 | + "*** Delete File: ", |
| 318 | + "*** Move to: ", |
| 319 | + } { |
| 320 | + if !strings.HasPrefix(line, prefix) { |
| 321 | + continue |
| 322 | + } |
| 323 | + file := strings.TrimSpace( |
| 324 | + strings.TrimPrefix(line, prefix), |
| 325 | + ) |
| 326 | + if file == "" { |
| 327 | + continue |
| 328 | + } |
| 329 | + if _, ok := seen[file]; ok { |
| 330 | + continue |
| 331 | + } |
| 332 | + seen[file] = struct{}{} |
| 333 | + files = append(files, file) |
| 334 | + break |
| 335 | + } |
| 336 | + } |
| 337 | + return files |
| 338 | +} |
| 339 | + |
| 340 | +func summarizePatchedFiles(files []string) string { |
| 341 | + switch len(files) { |
| 342 | + case 0: |
| 343 | + return "" |
| 344 | + case 1: |
| 345 | + return files[0] |
| 346 | + default: |
| 347 | + return fmt.Sprintf( |
| 348 | + "%s (+%d more)", |
| 349 | + files[0], len(files)-1, |
| 350 | + ) |
| 351 | + } |
| 352 | +} |
| 353 | + |
| 354 | +func codexCategoryDetail( |
| 355 | + category string, args gjson.Result, |
| 356 | +) string { |
| 357 | + switch category { |
| 358 | + case "Read", "Write", "Edit": |
| 359 | + return codexArgValue(args, "file_path", "path") |
| 360 | + case "Grep": |
| 361 | + return codexArgValue(args, "pattern") |
| 362 | + case "Glob": |
| 363 | + pattern := codexArgValue(args, "pattern") |
| 364 | + path := codexArgValue(args, "path") |
| 365 | + if pattern != "" && path != "" { |
| 366 | + return fmt.Sprintf("%s in %s", pattern, path) |
| 367 | + } |
| 368 | + return firstNonEmpty(pattern, path) |
| 369 | + case "Task": |
| 370 | + desc := codexArgValue(args, "description") |
| 371 | + agent := codexArgValue(args, "subagent_type") |
| 372 | + if desc != "" && agent != "" { |
| 373 | + return fmt.Sprintf("%s (%s)", desc, agent) |
| 374 | + } |
| 375 | + return firstNonEmpty(desc, agent) |
| 376 | + default: |
| 377 | + return "" |
| 378 | + } |
| 379 | +} |
| 380 | + |
| 381 | +func codexArgString( |
| 382 | + args gjson.Result, path string, |
| 383 | +) string { |
| 384 | + v := args.Get(path) |
| 385 | + if !v.Exists() { |
| 386 | + return "" |
| 387 | + } |
| 388 | + if v.Type == gjson.String { |
| 389 | + return v.Str |
| 390 | + } |
| 391 | + raw := strings.TrimSpace(v.Raw) |
| 392 | + if raw == "" || raw == "null" { |
| 393 | + return "" |
| 394 | + } |
| 395 | + return raw |
| 396 | +} |
| 397 | + |
| 398 | +func codexArgValue( |
| 399 | + args gjson.Result, paths ...string, |
| 400 | +) string { |
| 401 | + for _, path := range paths { |
| 402 | + v := strings.TrimSpace(codexArgString(args, path)) |
| 403 | + if v != "" { |
| 404 | + return v |
| 405 | + } |
| 406 | + } |
| 407 | + return "" |
| 408 | +} |
| 409 | + |
| 410 | +func codexArgPreview( |
| 411 | + args gjson.Result, rawArgs string, |
| 412 | +) string { |
| 413 | + if rawArgs != "" { |
| 414 | + flat := strings.Join( |
| 415 | + strings.Fields(rawArgs), " ", |
| 416 | + ) |
| 417 | + return truncate(flat, 220) |
| 418 | + } |
| 419 | + if args.Exists() { |
| 420 | + flat := strings.Join( |
| 421 | + strings.Fields(args.Raw), " ", |
| 422 | + ) |
| 423 | + if flat != "" { |
| 424 | + return truncate(flat, 220) |
| 425 | + } |
| 426 | + } |
| 427 | + return "" |
| 428 | +} |
| 429 | + |
| 430 | +func formatToolHeader( |
| 431 | + label, detail string, |
| 432 | +) string { |
| 433 | + label = sanitizeToolLabel(label) |
| 434 | + if label == "" { |
| 435 | + label = "Tool" |
| 436 | + } |
| 437 | + detail = sanitizeToolLabel(detail) |
| 438 | + if detail != "" { |
| 439 | + return fmt.Sprintf("[%s: %s]", label, detail) |
| 440 | + } |
| 441 | + return fmt.Sprintf("[%s]", label) |
| 442 | +} |
| 443 | + |
| 444 | +func sanitizeToolLabel(s string) string { |
| 445 | + s = strings.TrimSpace(s) |
| 446 | + if s == "" { |
| 447 | + return "" |
| 448 | + } |
| 449 | + s = strings.ReplaceAll(s, "]", ")") |
| 450 | + return strings.Join(strings.Fields(s), " ") |
| 451 | +} |
| 452 | + |
| 453 | +func firstNonEmpty(vals ...string) string { |
| 454 | + for _, v := range vals { |
| 455 | + v = strings.TrimSpace(v) |
| 456 | + if v != "" { |
| 457 | + return v |
| 458 | + } |
| 459 | + } |
| 460 | + return "" |
164 | 461 | } |
165 | 462 |
|
166 | 463 | // extractCodexContent joins all text blocks from a Codex |
|
0 commit comments