fix(sessions): retain matched search excerpts
- commit
- 2d08d0544d7f64a33b7c025881a2667a977415e2
- author
- brice · · 2026-08-19 07:08 UTC
- parent
- 108af61
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +49 -8
M packages/agent/modes/session_dialog.go +49 -8
| @@ -760,10 +760,11 @@ func formatSessionSearchRowPlain(s core.SessionSummary, maxWidth, matchCount int | |||
| 760 | 760 | } | |
| 761 | 761 | ||
| 762 | 762 | func highlightSessionSearchExcerpt(th tui.Theme, excerpt string, indexes []int, maxWidth int) string { | |
| 763 | - | excerpt = truncateSessionSearchExcerpt(excerpt, 240) | |
| 764 | - | if maxWidth > 0 && len([]rune(excerpt)) > maxWidth { | |
| 765 | - | excerpt = truncateSessionSearchExcerpt(excerpt, maxWidth) | |
| 763 | + | limit := 240 | |
| 764 | + | if maxWidth > 0 && maxWidth < limit { | |
| 765 | + | limit = maxWidth | |
| 766 | 766 | } | |
| 767 | + | excerpt, indexes = windowSessionSearchExcerpt(excerpt, indexes, limit) | |
| 767 | 768 | if len(indexes) == 0 { | |
| 768 | 769 | return th.FGColor(th.Muted, excerpt) | |
| 769 | 770 | } | |
| @@ -785,15 +786,55 @@ func highlightSessionSearchExcerpt(th tui.Theme, excerpt string, indexes []int, | |||
| 785 | 786 | return out.String() | |
| 786 | 787 | } | |
| 787 | 788 | ||
| 788 | - | func truncateSessionSearchExcerpt(text string, limit int) string { | |
| 789 | + | func windowSessionSearchExcerpt(text string, indexes []int, limit int) (string, []int) { | |
| 789 | 790 | runes := []rune(text) | |
| 790 | 791 | if limit <= 0 || len(runes) <= limit { | |
| 791 | - | return text | |
| 792 | + | return text, indexes | |
| 792 | 793 | } | |
| 793 | - | if limit <= 3 { | |
| 794 | - | return string(runes[:limit]) | |
| 794 | + | matchRune := 0 | |
| 795 | + | if len(indexes) > 0 { | |
| 796 | + | for runeIndex, byteIndex := range runeByteOffsets(text) { | |
| 797 | + | if byteIndex <= indexes[0] { | |
| 798 | + | matchRune = runeIndex | |
| 799 | + | } else { | |
| 800 | + | break | |
| 801 | + | } | |
| 802 | + | } | |
| 803 | + | } | |
| 804 | + | room := limit - 3 | |
| 805 | + | if room < 1 { | |
| 806 | + | return string(runes[:limit]), nil | |
| 807 | + | } | |
| 808 | + | start := max(0, matchRune-room/2) | |
| 809 | + | if start+room > len(runes) { | |
| 810 | + | start = len(runes) - room | |
| 811 | + | } | |
| 812 | + | end := min(len(runes), start+room) | |
| 813 | + | prefix := "" | |
| 814 | + | if start > 0 { | |
| 815 | + | prefix = "..." | |
| 816 | + | } | |
| 817 | + | suffix := "" | |
| 818 | + | if end < len(runes) { | |
| 819 | + | suffix = "..." | |
| 820 | + | } | |
| 821 | + | base := len(string(runes[:start])) | |
| 822 | + | stop := len(string(runes[:end])) | |
| 823 | + | rebased := make([]int, 0, len(indexes)) | |
| 824 | + | for _, index := range indexes { | |
| 825 | + | if index >= base && index < stop { | |
| 826 | + | rebased = append(rebased, len(prefix)+index-base) | |
| 827 | + | } | |
| 828 | + | } | |
| 829 | + | return prefix + string(runes[start:end]) + suffix, rebased | |
| 830 | + | } | |
| 831 | + | ||
| 832 | + | func runeByteOffsets(text string) []int { | |
| 833 | + | offsets := make([]int, 0, len(text)) | |
| 834 | + | for index := range text { | |
| 835 | + | offsets = append(offsets, index) | |
| 795 | 836 | } | |
| 796 | - | return string(runes[:limit-3]) + "..." | |
| 837 | + | return offsets | |
| 797 | 838 | } | |
| 798 | 839 | ||
| 799 | 840 | func formatSessionRowPlain(s core.SessionSummary, maxWidth int) string { | |
| @@ -760,10 +760,11 @@ func formatSessionSearchRowPlain(s core.SessionSummary, maxWidth, matchCount int | |||||
| 760 | } | 760 | } | ||
| 761 | 761 | ||||
| 762 | func highlightSessionSearchExcerpt(th tui.Theme, excerpt string, indexes []int, maxWidth int) string { | 762 | func highlightSessionSearchExcerpt(th tui.Theme, excerpt string, indexes []int, maxWidth int) string { | ||
| 763 | - | excerpt = truncateSessionSearchExcerpt(excerpt, 240) | 763 | + | limit := 240 |
| 764 | - | if maxWidth > 0 && len([]rune(excerpt)) > maxWidth { | 764 | + | if maxWidth > 0 && maxWidth < limit { |
| 765 | - | excerpt = truncateSessionSearchExcerpt(excerpt, maxWidth) | 765 | + | limit = maxWidth |
| 766 | } | 766 | } | ||
| 767 | + | excerpt, indexes = windowSessionSearchExcerpt(excerpt, indexes, limit) | |||
| 767 | if len(indexes) == 0 { | 768 | if len(indexes) == 0 { | ||
| 768 | return th.FGColor(th.Muted, excerpt) | 769 | return th.FGColor(th.Muted, excerpt) | ||
| 769 | } | 770 | } | ||
| @@ -785,15 +786,55 @@ func highlightSessionSearchExcerpt(th tui.Theme, excerpt string, indexes []int, | |||||
| 785 | return out.String() | 786 | return out.String() | ||
| 786 | } | 787 | } | ||
| 787 | 788 | ||||
| 788 | - | func truncateSessionSearchExcerpt(text string, limit int) string { | 789 | + | func windowSessionSearchExcerpt(text string, indexes []int, limit int) (string, []int) { |
| 789 | runes := []rune(text) | 790 | runes := []rune(text) | ||
| 790 | if limit <= 0 || len(runes) <= limit { | 791 | if limit <= 0 || len(runes) <= limit { | ||
| 791 | - | return text | 792 | + | return text, indexes |
| 792 | } | 793 | } | ||
| 793 | - | if limit <= 3 { | 794 | + | matchRune := 0 |
| 794 | - | return string(runes[:limit]) | 795 | + | if len(indexes) > 0 { |
| 796 | + | for runeIndex, byteIndex := range runeByteOffsets(text) { | |||
| 797 | + | if byteIndex <= indexes[0] { | |||
| 798 | + | matchRune = runeIndex | |||
| 799 | + | } else { | |||
| 800 | + | break | |||
| 801 | + | } | |||
| 802 | + | } | |||
| 803 | + | } | |||
| 804 | + | room := limit - 3 | |||
| 805 | + | if room < 1 { | |||
| 806 | + | return string(runes[:limit]), nil | |||
| 807 | + | } | |||
| 808 | + | start := max(0, matchRune-room/2) | |||
| 809 | + | if start+room > len(runes) { | |||
| 810 | + | start = len(runes) - room | |||
| 811 | + | } | |||
| 812 | + | end := min(len(runes), start+room) | |||
| 813 | + | prefix := "" | |||
| 814 | + | if start > 0 { | |||
| 815 | + | prefix = "..." | |||
| 816 | + | } | |||
| 817 | + | suffix := "" | |||
| 818 | + | if end < len(runes) { | |||
| 819 | + | suffix = "..." | |||
| 820 | + | } | |||
| 821 | + | base := len(string(runes[:start])) | |||
| 822 | + | stop := len(string(runes[:end])) | |||
| 823 | + | rebased := make([]int, 0, len(indexes)) | |||
| 824 | + | for _, index := range indexes { | |||
| 825 | + | if index >= base && index < stop { | |||
| 826 | + | rebased = append(rebased, len(prefix)+index-base) | |||
| 827 | + | } | |||
| 828 | + | } | |||
| 829 | + | return prefix + string(runes[start:end]) + suffix, rebased | |||
| 830 | + | } | |||
| 831 | + | ||||
| 832 | + | func runeByteOffsets(text string) []int { | |||
| 833 | + | offsets := make([]int, 0, len(text)) | |||
| 834 | + | for index := range text { | |||
| 835 | + | offsets = append(offsets, index) | |||
| 795 | } | 836 | } | ||
| 796 | - | return string(runes[:limit-3]) + "..." | 837 | + | return offsets |
| 797 | } | 838 | } | ||
| 798 | 839 | ||||
| 799 | func formatSessionRowPlain(s core.SessionSummary, maxWidth int) string { | 840 | func formatSessionRowPlain(s core.SessionSummary, maxWidth int) string { | ||