detached 2d08d05

fix(sessions): retain matched search excerpts

commit
2d08d0544d7f64a33b7c025881a2667a977415e2
author
brice · · 2026-08-19 07:08 UTC
parent
108af61
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
760760 }
761761
762762 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
766766 }
767+ excerpt, indexes = windowSessionSearchExcerpt(excerpt, indexes, limit)
767768 if len(indexes) == 0 {
768769 return th.FGColor(th.Muted, excerpt)
769770 }
@@ -785,15 +786,55 @@ func highlightSessionSearchExcerpt(th tui.Theme, excerpt string, indexes []int,
785786 return out.String()
786787 }
787788
788-func truncateSessionSearchExcerpt(text string, limit int) string {
789+func windowSessionSearchExcerpt(text string, indexes []int, limit int) (string, []int) {
789790 runes := []rune(text)
790791 if limit <= 0 || len(runes) <= limit {
791- return text
792+ return text, indexes
792793 }
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)
795836 }
796- return string(runes[:limit-3]) + "..."
837+ return offsets
797838 }
798839
799840 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 text792+ 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 {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close