Unified Agent + reusable Go agent core.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: address review feedback for PR #43

- Add missing clifmt.RenderMarkdown to fix compilation
- Unify resolveWritePath with tools/builtin/write_file.go logic:
- Add fileStateDir field to chatSession
- Use pathroots + pathutil for alias/absolute/relative resolution
- Enforce containment checks for absolute paths
- Fix snapshotProjectFiles performance:
- Cap at 500 files / 10MB total
- Skip snapshot for read-only bash commands (ls, cat, git status, etc.)
- Fix diff rendering for new/deleted files:
- write_file creating new file now shows full content diff
- bash deleting files now shows deletion notice
- bash creating new files now detected and shown
- Fix repl.go to use raw output for LLM history while keeping
rendered output for terminal display only
- gofmt formatting + go mod tidy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

authored by

Teteuya
Claude Sonnet 4.6
and committed by
Lyric Wai
b33bd9b0 3b3a67fa

+247 -104
+23
cmd/mistermorph/chatcmd/format.go
··· 9 9 "github.com/quailyquaily/mistermorph/internal/clifmt" 10 10 ) 11 11 12 + // formatRawChatOutput returns the raw assistant output without any terminal 13 + // formatting or ANSI codes. It is intended for use in LLM history. 14 + func formatRawChatOutput(final *agent.Final) string { 15 + if final == nil { 16 + return "" 17 + } 18 + switch output := final.Output.(type) { 19 + case string: 20 + return strings.TrimSpace(output) 21 + case nil: 22 + payload, _ := json.MarshalIndent(final, "", " ") 23 + return strings.TrimSpace(string(payload)) 24 + default: 25 + payload, err := json.MarshalIndent(output, "", " ") 26 + if err != nil { 27 + return strings.TrimSpace(fmt.Sprint(output)) 28 + } 29 + return strings.TrimSpace(string(payload)) 30 + } 31 + } 32 + 33 + // formatChatOutput returns the terminal-rendered version of the assistant 34 + // output, including Markdown/ANSI formatting for display. 12 35 func formatChatOutput(final *agent.Final) string { 13 36 if final == nil { 14 37 return ""
+6 -5
cmd/mistermorph/chatcmd/repl.go
··· 154 154 continue 155 155 } 156 156 157 - output := formatChatOutput(final) 157 + rawOutput := formatRawChatOutput(final) 158 + displayOutput := formatChatOutput(final) 158 159 if sess.compactMode { 159 - _, _ = fmt.Fprintf(writer, "%s\n", output) 160 + _, _ = fmt.Fprintf(writer, "%s\n", displayOutput) 160 161 } else { 161 - _, _ = fmt.Fprintf(writer, "\033[43m\033[30m %s> \033[0m %s\n", sess.agentName, output) 162 + _, _ = fmt.Fprintf(writer, "\033[43m\033[30m %s> \033[0m %s\n", sess.agentName, displayOutput) 162 163 } 163 164 164 165 history = append(history, 165 166 llm.Message{Role: "user", Content: input}, 166 - llm.Message{Role: "assistant", Content: output}, 167 + llm.Message{Role: "assistant", Content: rawOutput}, 167 168 ) 168 169 169 170 sess.logger.Info("chat_turn_done", ··· 174 175 ) 175 176 176 177 // Auto-update memory if there were tool calls 177 - autoUpdateMemory(writer, sess.logger, sess.memOrchestrator, sess.memWorker, sess.subjectID, runID, input, output, runCtx.Steps) 178 + autoUpdateMemory(writer, sess.logger, sess.memOrchestrator, sess.memWorker, sess.subjectID, runID, input, rawOutput, runCtx.Steps) 178 179 179 180 turn++ 180 181 }
+208 -45
cmd/mistermorph/chatcmd/session.go
··· 12 12 "time" 13 13 14 14 "github.com/quailyquaily/mistermorph/agent" 15 - "github.com/quailyquaily/mistermorph/internal/clifmt" 16 15 "github.com/quailyquaily/mistermorph/internal/acpclient" 16 + "github.com/quailyquaily/mistermorph/internal/clifmt" 17 17 "github.com/quailyquaily/mistermorph/internal/configutil" 18 18 "github.com/quailyquaily/mistermorph/internal/llmconfig" 19 19 "github.com/quailyquaily/mistermorph/internal/llmselect" ··· 22 22 "github.com/quailyquaily/mistermorph/internal/logutil" 23 23 "github.com/quailyquaily/mistermorph/internal/memoryruntime" 24 24 "github.com/quailyquaily/mistermorph/internal/pathroots" 25 + "github.com/quailyquaily/mistermorph/internal/pathutil" 25 26 "github.com/quailyquaily/mistermorph/internal/personautil" 26 27 "github.com/quailyquaily/mistermorph/internal/promptprofile" 27 28 "github.com/quailyquaily/mistermorph/internal/skillsutil" ··· 55 56 agentName string 56 57 launchDir string 57 58 fileCacheDir string 59 + fileStateDir string 58 60 workspaceDir string 59 61 sessionStore *llmselect.Store 60 62 llmValues llmutil.RuntimeValues ··· 244 246 return nil, err 245 247 } 246 248 launchDir = pathroots.New(launchDir, "", "").WorkspaceDir 249 + fileStateDir := strings.TrimSpace(viper.GetString("file_state_dir")) 250 + if fileStateDir == "" { 251 + fileStateDir = statepaths.FileStateDir() 252 + } 247 253 fileCacheDir := strings.TrimSpace(viper.GetString("file_cache_dir")) 248 254 rawWorkspace, _ := cmd.Flags().GetString("workspace") 249 255 noWorkspace, _ := cmd.Flags().GetBool("no-workspace") ··· 452 458 } 453 459 sess.fileSnapshots[path] = string(data) 454 460 } else if tc.Name == "bash" { 455 - sess.snapshotProjectFiles() 461 + if !isReadOnlyBashCommand(tc.Params) { 462 + sess.snapshotProjectFiles() 463 + } 456 464 } 457 465 })) 458 466 ··· 469 477 } 470 478 oldContent, hadOld := sess.fileSnapshots[resolvedPath] 471 479 delete(sess.fileSnapshots, resolvedPath) 480 + writer := sess.currentWriter() 472 481 if !hadOld { 473 - return // New file – no diff to show. 482 + // New file — show the full content as a diff from empty. 483 + newData, readErr := os.ReadFile(resolvedPath) 484 + if readErr != nil { 485 + return 486 + } 487 + diff := clifmt.RenderDiff(resolvedPath, "", string(newData)) 488 + if diff != "" { 489 + _, _ = fmt.Fprintln(writer, diff) 490 + } 491 + return 474 492 } 475 493 newData, readErr := os.ReadFile(resolvedPath) 476 494 if readErr != nil { ··· 480 498 if oldContent == newContent { 481 499 return // No change. 482 500 } 483 - writer := sess.currentWriter() 484 501 diff := clifmt.RenderDiff(resolvedPath, oldContent, newContent) 485 502 if diff != "" { 486 503 _, _ = fmt.Fprintln(writer, diff) ··· 546 563 makeEngine: makeEngine, 547 564 launchDir: launchDir, 548 565 fileCacheDir: fileCacheDir, 566 + fileStateDir: fileStateDir, 549 567 workspaceDir: workspaceDir, 550 568 basePromptSpec: promptSpec, 551 569 promptSpec: promptSpec, ··· 558 576 return sess, nil 559 577 } 560 578 561 - // resolveWritePath tries to resolve a write_file path to an absolute path 562 - // using the session's workspace, file cache, and launch directories. 563 - // It also handles workspace_dir/ and file_state_dir/ aliases like the 564 - // write_file tool does. 565 - func (s *chatSession) resolveWritePath(path string) string { 566 - path = strings.TrimSpace(path) 567 - if path == "" { 579 + // resolveWritePath resolves a write_file path to an absolute path, 580 + // matching the behavior of tools/builtin/write_file.go. 581 + func (s *chatSession) resolveWritePath(userPath string) string { 582 + roots := pathroots.New(s.workspaceDir, s.fileCacheDir, s.fileStateDir) 583 + if strings.TrimSpace(roots.FileCacheDir) == "" && strings.TrimSpace(roots.FileStateDir) == "" && strings.TrimSpace(roots.WorkspaceDir) == "" { 568 584 return "" 569 585 } 570 - if filepath.IsAbs(path) { 571 - return path 586 + 587 + userPath = pathutil.ExpandHomePath(userPath) 588 + userPath = strings.TrimSpace(userPath) 589 + if userPath == "" { 590 + return "" 591 + } 592 + 593 + // Alias handling: workspace_dir/..., file_cache_dir/..., file_state_dir/... 594 + trimmed := strings.TrimLeft(userPath, "/\\") 595 + lower := strings.ToLower(trimmed) 596 + prefixes := []struct { 597 + alias string 598 + prefix string 599 + }{ 600 + {"workspace_dir", "workspace_dir/"}, {"workspace_dir", "workspace_dir\\"}, 601 + {"file_cache_dir", "file_cache_dir/"}, {"file_cache_dir", "file_cache_dir\\"}, 602 + {"file_state_dir", "file_state_dir/"}, {"file_state_dir", "file_state_dir\\"}, 603 + } 604 + switch lower { 605 + case "workspace_dir", "file_cache_dir", "file_state_dir": 606 + return "" 607 + } 608 + for _, p := range prefixes { 609 + if !strings.HasPrefix(lower, p.prefix) { 610 + continue 611 + } 612 + base := strings.TrimSpace(roots.BaseDir(p.alias)) 613 + if base == "" { 614 + return "" 615 + } 616 + baseAbs, _ := filepath.Abs(base) 617 + rest := strings.TrimLeft(trimmed[len(p.prefix):], "/\\") 618 + if rest == "" { 619 + return "" 620 + } 621 + cand := filepath.Join(baseAbs, rest) 622 + candAbs, _ := filepath.Abs(cand) 623 + if !pathutil.IsWithinDir(baseAbs, candAbs) { 624 + return "" 625 + } 626 + return candAbs 572 627 } 573 628 574 - // Handle aliases like "workspace_dir/hello.go" or "file_state_dir/config.yaml". 575 - if parts := strings.SplitN(path, "/", 2); len(parts) == 2 { 576 - switch parts[0] { 577 - case "workspace_dir": 578 - if s.workspaceDir != "" { 579 - return filepath.Join(s.workspaceDir, parts[1]) 629 + // Absolute path — must be within allowed base dirs. 630 + if filepath.IsAbs(userPath) { 631 + candAbs, err := filepath.Abs(filepath.Clean(userPath)) 632 + if err != nil { 633 + return "" 634 + } 635 + for _, base := range roots.AllowedBaseDirs() { 636 + baseAbs, err := filepath.Abs(base) 637 + if err != nil { 638 + continue 580 639 } 581 - case "file_cache_dir": 582 - if s.fileCacheDir != "" { 583 - return filepath.Join(s.fileCacheDir, parts[1]) 584 - } 585 - case "file_state_dir": 586 - if s.launchDir != "" { 587 - return filepath.Join(s.launchDir, parts[1]) 640 + if pathutil.IsWithinDir(baseAbs, candAbs) || filepath.Clean(baseAbs) == filepath.Clean(candAbs) { 641 + return candAbs 588 642 } 589 643 } 644 + return "" 590 645 } 591 646 592 - candidates := []string{} 593 - if s.workspaceDir != "" { 594 - candidates = append(candidates, filepath.Join(s.workspaceDir, path)) 647 + // Relative path — resolved under the default base dir. 648 + defaultBase := strings.TrimSpace(roots.DefaultFileDir()) 649 + if defaultBase == "" { 650 + return "" 595 651 } 596 - if s.fileCacheDir != "" { 597 - candidates = append(candidates, filepath.Join(s.fileCacheDir, path)) 652 + baseAbs, _ := filepath.Abs(defaultBase) 653 + userPath = strings.TrimLeft(strings.TrimSpace(userPath), "/\\") 654 + if userPath == "" { 655 + return "" 598 656 } 599 - if s.launchDir != "" { 600 - candidates = append(candidates, filepath.Join(s.launchDir, path)) 657 + cand := filepath.Join(baseAbs, userPath) 658 + candAbs, _ := filepath.Abs(cand) 659 + if !pathutil.IsWithinDir(baseAbs, candAbs) { 660 + return "" 601 661 } 602 - for _, cand := range candidates { 603 - if _, err := os.Stat(cand); err == nil { 604 - return cand 605 - } 606 - } 607 - // Fallback to first candidate even if file doesn't exist yet. 608 - if len(candidates) > 0 { 609 - return candidates[0] 610 - } 611 - return "" 662 + return candAbs 612 663 } 613 664 614 665 func (s *chatSession) cleanup() { ··· 618 669 } 619 670 620 671 // snapshotProjectFiles scans the project directory and stores the current 621 - // content of all existing text files into fileSnapshots. 672 + // content of existing text files into fileSnapshots. It caps the number of 673 + // files and total bytes to avoid bloating memory on large repos. 622 674 func (s *chatSession) snapshotProjectFiles() { 623 675 if s == nil { 624 676 return ··· 627 679 if dir == "" { 628 680 return 629 681 } 682 + const maxFiles = 500 683 + const maxTotalBytes = 10 * 1024 * 1024 684 + fileCount := 0 685 + totalBytes := 0 630 686 _ = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { 631 687 if err != nil { 632 688 return nil ··· 640 696 return filepath.SkipDir 641 697 } 642 698 return nil 699 + } 700 + if fileCount >= maxFiles || totalBytes >= maxTotalBytes { 701 + return filepath.SkipAll 643 702 } 644 703 info, err := d.Info() 645 704 if err != nil { ··· 656 715 return nil 657 716 } 658 717 s.fileSnapshots[path] = string(data) 718 + fileCount++ 719 + totalBytes += len(data) 659 720 return nil 660 721 }) 661 722 } 662 723 724 + // isReadOnlyBashCommand heuristically detects bash commands that are unlikely 725 + // to modify files, so we can skip the expensive snapshot step. 726 + func isReadOnlyBashCommand(params map[string]any) bool { 727 + cmd, _ := params["command"].(string) 728 + cmd = strings.TrimSpace(cmd) 729 + if cmd == "" { 730 + return false 731 + } 732 + readOnlyPrefixes := []string{ 733 + "ls ", "ls\t", "ls\n", 734 + "cat ", "cat\t", 735 + "find ", "find\t", 736 + "grep ", "grep\t", 737 + "git status", "git log", "git diff", "git show", "git branch", 738 + "go test", "go vet", "go mod", "go list", "go env", 739 + "echo ", "echo\t", 740 + "pwd", "pwd ", "pwd\t", 741 + "head ", "head\t", "tail ", "tail\t", 742 + "wc ", "wc\t", 743 + "sort ", "sort\t", "uniq ", "uniq\t", 744 + "ps ", "ps\t", "top", "htop", 745 + "df ", "df\t", "du ", "du\t", 746 + "curl ", "curl\t", "wget ", "wget\t", 747 + "which ", "which\t", "whereis ", "whereis\t", 748 + } 749 + lower := strings.ToLower(cmd) 750 + for _, prefix := range readOnlyPrefixes { 751 + if strings.HasPrefix(lower, prefix) { 752 + return true 753 + } 754 + } 755 + return false 756 + } 757 + 663 758 func isBinaryData(data []byte) bool { 664 759 limit := 8192 665 760 if len(data) < limit { ··· 674 769 } 675 770 676 771 // renderBashDiffs compares fileSnapshots against current disk content and 677 - // renders diffs for any files that changed during a bash tool call. 772 + // renders diffs for any files that changed, were deleted, or were created 773 + // during a bash tool call. 678 774 func (s *chatSession) renderBashDiffs() { 679 775 if s == nil { 680 776 return 681 777 } 682 778 writer := s.currentWriter() 779 + 780 + // Remember which paths were snapshotted before bash ran. 781 + oldPaths := make(map[string]struct{}, len(s.fileSnapshots)) 782 + for path := range s.fileSnapshots { 783 + oldPaths[path] = struct{}{} 784 + } 785 + 786 + // Show diffs for changed or deleted files. 683 787 for path, oldContent := range s.fileSnapshots { 684 788 newData, err := os.ReadFile(path) 685 789 delete(s.fileSnapshots, path) 686 790 if err != nil { 791 + if os.IsNotExist(err) { 792 + _, _ = fmt.Fprintf(writer, "\x1b[33m deleted %s\x1b[0m\n", path) 793 + } 687 794 continue 688 795 } 689 796 newContent := string(newData) ··· 695 802 _, _ = fmt.Fprintln(writer, diff) 696 803 } 697 804 } 805 + 806 + // Show newly created files. 807 + s.showNewFiles(writer, oldPaths) 808 + } 809 + 810 + // showNewFiles scans the project directory for files that were not present 811 + // in the pre-bash snapshot and renders their full content as a diff from empty. 812 + func (s *chatSession) showNewFiles(writer io.Writer, oldPaths map[string]struct{}) { 813 + dir := s.projectDir() 814 + if dir == "" { 815 + return 816 + } 817 + const maxNewFiles = 50 818 + const maxBytesPerFile = 1024 * 1024 819 + newFileCount := 0 820 + _ = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { 821 + if err != nil { 822 + return nil 823 + } 824 + if d.IsDir() { 825 + name := d.Name() 826 + if strings.HasPrefix(name, ".") && path != dir { 827 + return filepath.SkipDir 828 + } 829 + if name == "node_modules" || name == "vendor" || name == "target" || name == "dist" || name == "build" { 830 + return filepath.SkipDir 831 + } 832 + return nil 833 + } 834 + if _, known := oldPaths[path]; known { 835 + return nil 836 + } 837 + if newFileCount >= maxNewFiles { 838 + return filepath.SkipAll 839 + } 840 + info, err := d.Info() 841 + if err != nil { 842 + return nil 843 + } 844 + if info.Size() > maxBytesPerFile { 845 + return nil 846 + } 847 + data, err := os.ReadFile(path) 848 + if err != nil { 849 + return nil 850 + } 851 + if isBinaryData(data) { 852 + return nil 853 + } 854 + diff := clifmt.RenderDiff(path, "", string(data)) 855 + if diff != "" { 856 + _, _ = fmt.Fprintln(writer, diff) 857 + } 858 + newFileCount++ 859 + return nil 860 + }) 698 861 }
+2 -10
go.mod
··· 6 6 github.com/alecthomas/chroma/v2 v2.23.1 7 7 github.com/aws/aws-sdk-go-v2/config v1.32.16 8 8 github.com/aws/aws-sdk-go-v2/credentials v1.19.15 9 - github.com/charmbracelet/glamour v1.0.0 10 9 github.com/chzyer/readline v1.5.1 11 10 github.com/google/uuid v1.6.0 12 11 github.com/gorilla/websocket v1.5.3 13 12 github.com/lyricat/goutils v1.2.3 13 + github.com/mattn/go-runewidth v0.0.19 14 14 github.com/modelcontextprotocol/go-sdk v1.4.0 15 15 github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1 16 16 github.com/openai/openai-go/v3 v3.2.0 ··· 49 49 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect 50 50 github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect 51 51 github.com/aws/smithy-go v1.25.0 // indirect 52 - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 53 - github.com/aymerick/douceur v0.2.0 // indirect 54 52 github.com/bep/debounce v1.2.1 // indirect 55 - github.com/charmbracelet/colorprofile v0.4.1 // indirect 56 - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect 57 - github.com/charmbracelet/x/ansi v0.11.4 // indirect 58 - github.com/charmbracelet/x/cellbuf v0.0.14 // indirect 59 - github.com/charmbracelet/x/exp/slice v0.0.0-20260122224438-b01af16209d9 // indirect 60 - github.com/charmbracelet/x/term v0.2.2 // indirect 61 - github.com/clipperhouse/displaywidth v0.7.0 // indirect 62 53 github.com/clipperhouse/stringish v0.1.1 // indirect 63 54 github.com/clipperhouse/uax29/v2 v2.4.0 // indirect 64 55 github.com/cloudflare/circl v1.6.3 // indirect 65 56 github.com/coder/websocket v1.8.14 // indirect 66 57 github.com/cyphar/filepath-securejoin v0.6.1 // indirect 58 + github.com/dlclark/regexp2 v1.11.5 // indirect 67 59 github.com/dsnet/compress v0.0.1 // indirect 68 60 github.com/ebitengine/purego v0.9.1 // indirect 69 61 github.com/emirpasic/gods v1.18.1 // indirect
-40
go.sum
··· 55 55 github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo= 56 56 github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= 57 57 github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= 58 - github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 59 - github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 60 - github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 61 - github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 62 - github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 63 - github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 64 58 github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= 65 59 github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= 66 - github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= 67 - github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= 68 - github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= 69 - github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= 70 - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= 71 - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= 72 - github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI= 73 - github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4= 74 - github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= 75 - github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= 76 - github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= 77 - github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 78 - github.com/charmbracelet/x/exp/slice v0.0.0-20260122224438-b01af16209d9 h1:BBTx26Fy+CW9U3kLiWBuWn9pI9C1NybaS+p/AZeAOkA= 79 - github.com/charmbracelet/x/exp/slice v0.0.0-20260122224438-b01af16209d9/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= 80 - github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= 81 - github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= 82 60 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 83 61 github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 84 62 github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 85 63 github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 86 64 github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 87 65 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 88 - github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE= 89 - github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= 90 66 github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= 91 67 github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= 92 68 github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g= ··· 146 122 github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= 147 123 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 148 124 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 149 - github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 150 - github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 151 125 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 152 126 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 153 127 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= ··· 185 159 github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= 186 160 github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= 187 161 github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 188 - github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 189 - github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 190 162 github.com/lyricat/goutils v1.2.3 h1:bJCYygnCYwELtXrzeA/oW0Xl1aMRMutpzyWqfF5AvJI= 191 163 github.com/lyricat/goutils v1.2.3/go.mod h1:AscmPHLrB2accCEVP4gSI6y3ezcud3zHM1w3t7M/jNU= 192 164 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= ··· 198 170 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 199 171 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 200 172 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 201 - github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 202 173 github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= 203 174 github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 204 175 github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU= 205 176 github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= 206 - github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 207 - github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 208 177 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 209 178 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 210 179 github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8= 211 180 github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E= 212 - github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 213 - github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 214 - github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 215 - github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 216 181 github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84 h1:/6MoQlTdk1eAi0J9O89ypO8umkp+H7mpnSF2ggSL62Q= 217 182 github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84/go.mod h1:Eeech2fhQ/E4bS8cdc3+SGABQ+weQYGyWBvZ/mNr5uY= 218 183 github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1 h1:9awJsNP+gYOGCr3pQu9i217bCNsVwoQCmD3h7CYwxOw= ··· 238 203 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 239 204 github.com/quailyquaily/uniai v0.1.21 h1:Z5LarPPOVXp5JjVVkA9foe3rTfjnZRW/2mGzswG308Q= 240 205 github.com/quailyquaily/uniai v0.1.21/go.mod h1:C3kQuLcZ+QvU1+uRmRXkHx3Jo8gaZxa6Da54aUI9nj4= 241 - github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 242 206 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 243 207 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 244 208 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= ··· 312 276 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 313 277 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 314 278 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 315 - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 316 - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 317 279 github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= 318 280 github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 319 281 github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= 320 282 github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 321 - github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= 322 - github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= 323 283 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 324 284 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 325 285 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+4
internal/clifmt/clifmt.go
··· 38 38 return "\x1b[" + code + "m" + text + "\x1b[0m" 39 39 } 40 40 41 + func RenderMarkdown(text string) string { 42 + return HighlightCodeBlocks(text) 43 + } 44 + 41 45 func useColor() bool { 42 46 if os.Getenv("NO_COLOR") != "" { 43 47 return false
+3 -3
internal/clifmt/diff.go
··· 10 10 11 11 // DiffLine represents a single line in a unified diff output. 12 12 type diffLine struct { 13 - kind byte // ' ' context, '-' delete, '+' insert 13 + kind byte // ' ' context, '-' delete, '+' insert 14 14 text string 15 - oldNum int // 0 if not present in old file 16 - newNum int // 0 if not present in new file 15 + oldNum int // 0 if not present in old file 16 + newNum int // 0 if not present in new file 17 17 } 18 18 19 19 // splitLines splits a string into lines, discarding the final empty element
+1 -1
internal/clifmt/syntax.go
··· 7 7 "strings" 8 8 9 9 "github.com/alecthomas/chroma/v2" 10 - "github.com/mattn/go-runewidth" 11 10 "github.com/alecthomas/chroma/v2/formatters" 12 11 "github.com/alecthomas/chroma/v2/lexers" 13 12 "github.com/alecthomas/chroma/v2/styles" 13 + "github.com/mattn/go-runewidth" 14 14 ) 15 15 16 16 var codeBlockRe = regexp.MustCompile("(?s)```(\\w*)\\n(.*?)```")