gora-cli 0.1.3__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gora_cli-0.1.3 → gora_cli-0.1.4}/PKG-INFO +1 -1
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/__init__.py +1 -1
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/go_tui/main.go +232 -41
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/tui.py +1 -1
- {gora_cli-0.1.3 → gora_cli-0.1.4}/pyproject.toml +1 -1
- {gora_cli-0.1.3 → gora_cli-0.1.4}/uv.lock +1 -1
- {gora_cli-0.1.3 → gora_cli-0.1.4}/.gitignore +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/README.md +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/__main__.py +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/cli.py +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/go_tui/go.mod +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/go_tui/go.sum +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/go_tui/main_test.go +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/parsers.py +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/gora/store.py +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/images/gora-header.png +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/main.py +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/tests/__init__.py +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/tests/test_parsers.py +0 -0
- {gora_cli-0.1.3 → gora_cli-0.1.4}/tests/test_store.py +0 -0
|
@@ -69,6 +69,14 @@ type resultsLoadedMsg struct {
|
|
|
69
69
|
err error
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
type previewLoadedMsg struct {
|
|
73
|
+
token int
|
|
74
|
+
sessionKey string
|
|
75
|
+
allRoles bool
|
|
76
|
+
messages []transcriptMessage
|
|
77
|
+
err error
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
type item struct {
|
|
73
81
|
SessionKey string
|
|
74
82
|
Provider string
|
|
@@ -375,10 +383,11 @@ type model struct {
|
|
|
375
383
|
focus focusArea
|
|
376
384
|
filterSection filterSection
|
|
377
385
|
previewKey string
|
|
378
|
-
previewWidth int
|
|
379
386
|
previewAllRoles bool
|
|
380
|
-
|
|
387
|
+
previewLoading bool
|
|
388
|
+
previewMessages []transcriptMessage
|
|
381
389
|
previewErr error
|
|
390
|
+
previewToken int
|
|
382
391
|
selected int
|
|
383
392
|
width int
|
|
384
393
|
height int
|
|
@@ -400,6 +409,7 @@ type model struct {
|
|
|
400
409
|
tableRowsWidth int
|
|
401
410
|
tableRowsHeight int
|
|
402
411
|
tableRowsSelected int
|
|
412
|
+
tableRows []table.Row
|
|
403
413
|
notice string
|
|
404
414
|
err error
|
|
405
415
|
}
|
|
@@ -421,7 +431,9 @@ func main() {
|
|
|
421
431
|
os.Exit(1)
|
|
422
432
|
}
|
|
423
433
|
defer db.Close()
|
|
424
|
-
db.SetMaxOpenConns(
|
|
434
|
+
db.SetMaxOpenConns(4)
|
|
435
|
+
db.SetMaxIdleConns(4)
|
|
436
|
+
_, _ = db.Exec("PRAGMA busy_timeout = 1000")
|
|
425
437
|
|
|
426
438
|
search := textinput.New()
|
|
427
439
|
search.Placeholder = "type to search local Codex, Claude Code, and Pi chats"
|
|
@@ -490,7 +502,9 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
490
502
|
}
|
|
491
503
|
return m, m.loadResultsCmd(msg.token)
|
|
492
504
|
case resultsLoadedMsg:
|
|
493
|
-
m.applyResults(msg)
|
|
505
|
+
return m, m.applyResults(msg)
|
|
506
|
+
case previewLoadedMsg:
|
|
507
|
+
m.applyPreview(msg)
|
|
494
508
|
}
|
|
495
509
|
return m, nil
|
|
496
510
|
}
|
|
@@ -544,17 +558,17 @@ func (m *model) handleKey(msg tea.KeyMsg) tea.Cmd {
|
|
|
544
558
|
case "/":
|
|
545
559
|
m.setFocus(focusSearch)
|
|
546
560
|
case "up", "k":
|
|
547
|
-
m.move(-1)
|
|
548
561
|
m.setFocus(focusResults)
|
|
562
|
+
return m.move(-1)
|
|
549
563
|
case "down", "j":
|
|
550
|
-
m.move(1)
|
|
551
564
|
m.setFocus(focusResults)
|
|
565
|
+
return m.move(1)
|
|
552
566
|
case "pgup":
|
|
553
|
-
m.move(-10)
|
|
554
567
|
m.setFocus(focusResults)
|
|
568
|
+
return m.move(-10)
|
|
555
569
|
case "pgdown":
|
|
556
|
-
m.move(10)
|
|
557
570
|
m.setFocus(focusResults)
|
|
571
|
+
return m.move(10)
|
|
558
572
|
case "enter", "right":
|
|
559
573
|
if m.focus == focusFilters {
|
|
560
574
|
m.openFilters()
|
|
@@ -574,6 +588,9 @@ func (m *model) handleKey(msg tea.KeyMsg) tea.Cmd {
|
|
|
574
588
|
return m.requestReload(0)
|
|
575
589
|
default:
|
|
576
590
|
if m.focus != focusSearch {
|
|
591
|
+
if key == "q" {
|
|
592
|
+
return tea.Quit
|
|
593
|
+
}
|
|
577
594
|
if key == "m" {
|
|
578
595
|
m.exportSelected("md")
|
|
579
596
|
return nil
|
|
@@ -613,14 +630,12 @@ func (m *model) handleMouse(msg tea.MouseMsg) tea.Cmd {
|
|
|
613
630
|
return cmd
|
|
614
631
|
}
|
|
615
632
|
if msg.Button == tea.MouseButtonWheelUp {
|
|
616
|
-
m.move(-3)
|
|
617
633
|
m.setFocus(focusResults)
|
|
618
|
-
return
|
|
634
|
+
return m.move(-3)
|
|
619
635
|
}
|
|
620
636
|
if msg.Button == tea.MouseButtonWheelDown {
|
|
621
|
-
m.move(3)
|
|
622
637
|
m.setFocus(focusResults)
|
|
623
|
-
return
|
|
638
|
+
return m.move(3)
|
|
624
639
|
}
|
|
625
640
|
if msg.Button != tea.MouseButtonLeft || msg.Action != tea.MouseActionPress {
|
|
626
641
|
return nil
|
|
@@ -953,31 +968,20 @@ func clipStyledLines(lines []string, height int) []string {
|
|
|
953
968
|
}
|
|
954
969
|
|
|
955
970
|
func (m *model) transcriptPreviewLines(row item, width int) []string {
|
|
956
|
-
if m.
|
|
971
|
+
if m.previewKey != row.SessionKey || m.previewAllRoles != m.allRoles || m.previewLoading {
|
|
972
|
+
return []string{mutedStyle.Render("loading preview...")}
|
|
973
|
+
}
|
|
974
|
+
if m.previewErr != nil {
|
|
975
|
+
return []string{lipgloss.NewStyle().Foreground(red).Render(fit(m.previewErr.Error(), width))}
|
|
976
|
+
}
|
|
977
|
+
if len(m.previewMessages) == 0 {
|
|
957
978
|
fallback := "session " + row.SessionKey
|
|
958
979
|
if row.Preview != "" {
|
|
959
980
|
fallback = "match " + row.Preview
|
|
960
981
|
}
|
|
961
982
|
return []string{mutedStyle.Render(fit(fallback, width))}
|
|
962
983
|
}
|
|
963
|
-
|
|
964
|
-
if m.previewErr != nil {
|
|
965
|
-
return []string{lipgloss.NewStyle().Foreground(red).Render(fit(m.previewErr.Error(), width))}
|
|
966
|
-
}
|
|
967
|
-
return m.previewLines
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
messages, err := loadTranscriptPreviewMessages(m.db, row.SessionKey, m.allRoles, 8)
|
|
971
|
-
m.previewKey = row.SessionKey
|
|
972
|
-
m.previewWidth = width
|
|
973
|
-
m.previewAllRoles = m.allRoles
|
|
974
|
-
m.previewErr = err
|
|
975
|
-
if err != nil {
|
|
976
|
-
m.previewLines = nil
|
|
977
|
-
return []string{lipgloss.NewStyle().Foreground(red).Render(fit(err.Error(), width))}
|
|
978
|
-
}
|
|
979
|
-
m.previewLines = formatTranscriptPreviewLines(messages, width)
|
|
980
|
-
return m.previewLines
|
|
984
|
+
return formatTranscriptPreviewLines(m.previewMessages, width)
|
|
981
985
|
}
|
|
982
986
|
|
|
983
987
|
func formatTranscriptPreviewLines(messages []transcriptMessage, width int) []string {
|
|
@@ -1023,14 +1027,18 @@ func (m *model) configureResultsTable(width int, height int) {
|
|
|
1023
1027
|
height = max(4, height)
|
|
1024
1028
|
if m.tableRowsVersion != m.resultsVersion ||
|
|
1025
1029
|
m.tableRowsWidth != width ||
|
|
1026
|
-
m.tableRowsHeight != height
|
|
1027
|
-
m.tableRowsSelected != m.selected {
|
|
1030
|
+
m.tableRowsHeight != height {
|
|
1028
1031
|
m.resultsTable.SetColumns(resultTableColumns(width))
|
|
1029
|
-
m.
|
|
1032
|
+
m.tableRows = resultTableRows(m.results, m.selected)
|
|
1033
|
+
m.resultsTable.SetRows(m.tableRows)
|
|
1030
1034
|
m.tableRowsVersion = m.resultsVersion
|
|
1031
1035
|
m.tableRowsWidth = width
|
|
1032
1036
|
m.tableRowsHeight = height
|
|
1033
1037
|
m.tableRowsSelected = m.selected
|
|
1038
|
+
} else if m.tableRowsSelected != m.selected {
|
|
1039
|
+
updateSelectedMarker(m.tableRows, m.tableRowsSelected, m.selected)
|
|
1040
|
+
m.resultsTable.SetRows(m.tableRows)
|
|
1041
|
+
m.tableRowsSelected = m.selected
|
|
1034
1042
|
}
|
|
1035
1043
|
m.resultsTable.SetWidth(width)
|
|
1036
1044
|
m.resultsTable.SetHeight(height)
|
|
@@ -1044,6 +1052,15 @@ func (m *model) configureResultsTable(width int, height int) {
|
|
|
1044
1052
|
}
|
|
1045
1053
|
}
|
|
1046
1054
|
|
|
1055
|
+
func updateSelectedMarker(rows []table.Row, oldSelected int, newSelected int) {
|
|
1056
|
+
if oldSelected >= 0 && oldSelected < len(rows) && len(rows[oldSelected]) > 0 {
|
|
1057
|
+
rows[oldSelected][0] = ""
|
|
1058
|
+
}
|
|
1059
|
+
if newSelected >= 0 && newSelected < len(rows) && len(rows[newSelected]) > 0 {
|
|
1060
|
+
rows[newSelected][0] = ">"
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1047
1064
|
func resultTableColumns(width int) []table.Column {
|
|
1048
1065
|
inner := max(40, width-4)
|
|
1049
1066
|
markerWidth := 2
|
|
@@ -1437,17 +1454,23 @@ func (m *model) renderDetailStatus(width int) string {
|
|
|
1437
1454
|
return ""
|
|
1438
1455
|
}
|
|
1439
1456
|
|
|
1440
|
-
func (m *model) move(delta int) {
|
|
1457
|
+
func (m *model) move(delta int) tea.Cmd {
|
|
1441
1458
|
if len(m.results) == 0 {
|
|
1442
1459
|
m.selected = 0
|
|
1443
|
-
return
|
|
1460
|
+
return nil
|
|
1444
1461
|
}
|
|
1462
|
+
before := m.selected
|
|
1445
1463
|
if delta < 0 {
|
|
1446
1464
|
m.resultsTable.MoveUp(-delta)
|
|
1447
1465
|
} else {
|
|
1448
1466
|
m.resultsTable.MoveDown(delta)
|
|
1449
1467
|
}
|
|
1450
1468
|
m.selected = clamp(m.resultsTable.Cursor(), 0, len(m.results)-1)
|
|
1469
|
+
m.configureResultsTable(max(72, m.tableRowsWidth), max(4, m.tableRowsHeight))
|
|
1470
|
+
if m.selected == before {
|
|
1471
|
+
return nil
|
|
1472
|
+
}
|
|
1473
|
+
return m.requestPreview()
|
|
1451
1474
|
}
|
|
1452
1475
|
|
|
1453
1476
|
func (m *model) openDetail() {
|
|
@@ -1831,6 +1854,52 @@ func (m *model) requestReload(delay time.Duration) tea.Cmd {
|
|
|
1831
1854
|
return m.loadResultsCmd(token)
|
|
1832
1855
|
}
|
|
1833
1856
|
|
|
1857
|
+
func (m *model) requestPreview() tea.Cmd {
|
|
1858
|
+
row, ok := m.currentSelection()
|
|
1859
|
+
if !ok || m.db == nil {
|
|
1860
|
+
m.previewKey = ""
|
|
1861
|
+
m.previewMessages = nil
|
|
1862
|
+
m.previewErr = nil
|
|
1863
|
+
m.previewLoading = false
|
|
1864
|
+
return nil
|
|
1865
|
+
}
|
|
1866
|
+
if m.previewKey == row.SessionKey && m.previewAllRoles == m.allRoles && !m.previewLoading && m.previewErr == nil && len(m.previewMessages) > 0 {
|
|
1867
|
+
return nil
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
m.previewToken++
|
|
1871
|
+
token := m.previewToken
|
|
1872
|
+
sessionKey := row.SessionKey
|
|
1873
|
+
allRoles := m.allRoles
|
|
1874
|
+
db := m.db
|
|
1875
|
+
|
|
1876
|
+
m.previewKey = sessionKey
|
|
1877
|
+
m.previewAllRoles = allRoles
|
|
1878
|
+
m.previewMessages = nil
|
|
1879
|
+
m.previewErr = nil
|
|
1880
|
+
m.previewLoading = true
|
|
1881
|
+
|
|
1882
|
+
return func() tea.Msg {
|
|
1883
|
+
messages, err := loadTranscriptPreviewMessages(db, sessionKey, allRoles, 8)
|
|
1884
|
+
return previewLoadedMsg{
|
|
1885
|
+
token: token,
|
|
1886
|
+
sessionKey: sessionKey,
|
|
1887
|
+
allRoles: allRoles,
|
|
1888
|
+
messages: messages,
|
|
1889
|
+
err: err,
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
func (m *model) applyPreview(msg previewLoadedMsg) {
|
|
1895
|
+
if msg.token != m.previewToken || msg.sessionKey != m.previewKey || msg.allRoles != m.previewAllRoles {
|
|
1896
|
+
return
|
|
1897
|
+
}
|
|
1898
|
+
m.previewLoading = false
|
|
1899
|
+
m.previewMessages = msg.messages
|
|
1900
|
+
m.previewErr = msg.err
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1834
1903
|
func (m *model) loadResultsCmd(token int) tea.Cmd {
|
|
1835
1904
|
ctx, cancel := context.WithCancel(context.Background())
|
|
1836
1905
|
m.cancelReload = cancel
|
|
@@ -1849,7 +1918,7 @@ func (m *model) loadResultsCmd(token int) tea.Cmd {
|
|
|
1849
1918
|
if query == "" {
|
|
1850
1919
|
rows, err = listSessionsContext(ctx, db, providers, models, repos)
|
|
1851
1920
|
} else {
|
|
1852
|
-
rows, truncated, err =
|
|
1921
|
+
rows, truncated, err = searchMessagesTUIContext(ctx, db, query, providers, models, repos, allRoles, tuiSearchLimit)
|
|
1853
1922
|
}
|
|
1854
1923
|
return resultsLoadedMsg{
|
|
1855
1924
|
token: token,
|
|
@@ -1861,9 +1930,9 @@ func (m *model) loadResultsCmd(token int) tea.Cmd {
|
|
|
1861
1930
|
}
|
|
1862
1931
|
}
|
|
1863
1932
|
|
|
1864
|
-
func (m *model) applyResults(msg resultsLoadedMsg) {
|
|
1933
|
+
func (m *model) applyResults(msg resultsLoadedMsg) tea.Cmd {
|
|
1865
1934
|
if msg.token != m.reloadToken {
|
|
1866
|
-
return
|
|
1935
|
+
return nil
|
|
1867
1936
|
}
|
|
1868
1937
|
m.loading = false
|
|
1869
1938
|
m.searchTooShort = msg.tooShort
|
|
@@ -1871,12 +1940,12 @@ func (m *model) applyResults(msg resultsLoadedMsg) {
|
|
|
1871
1940
|
m.cancelReload = nil
|
|
1872
1941
|
if msg.err != nil {
|
|
1873
1942
|
if errors.Is(msg.err, context.Canceled) {
|
|
1874
|
-
return
|
|
1943
|
+
return nil
|
|
1875
1944
|
}
|
|
1876
1945
|
m.err = msg.err
|
|
1877
1946
|
m.results = nil
|
|
1878
1947
|
m.resultsVersion++
|
|
1879
|
-
return
|
|
1948
|
+
return nil
|
|
1880
1949
|
}
|
|
1881
1950
|
m.err = nil
|
|
1882
1951
|
m.results = msg.rows
|
|
@@ -1886,6 +1955,7 @@ func (m *model) applyResults(msg resultsLoadedMsg) {
|
|
|
1886
1955
|
m.resultsVersion++
|
|
1887
1956
|
m.previewKey = ""
|
|
1888
1957
|
m.configureResultsTable(max(72, m.width), max(4, m.height/2))
|
|
1958
|
+
return m.requestPreview()
|
|
1889
1959
|
}
|
|
1890
1960
|
|
|
1891
1961
|
func (m *model) resetPosition() {
|
|
@@ -2055,6 +2125,113 @@ func searchMessagesContext(ctx context.Context, db *sql.DB, query string, provid
|
|
|
2055
2125
|
return searchMessagesLike(ctx, db, query, providers, models, repos, allRoles, limit)
|
|
2056
2126
|
}
|
|
2057
2127
|
|
|
2128
|
+
func searchMessagesTUIContext(ctx context.Context, db *sql.DB, query string, providers []string, models []string, repos []string, allRoles bool, limit int) ([]item, bool, error) {
|
|
2129
|
+
rows, truncated, err := searchMessagesFTSFast(ctx, db, query, providers, models, repos, allRoles, limit)
|
|
2130
|
+
if err == nil {
|
|
2131
|
+
return rows, truncated, nil
|
|
2132
|
+
}
|
|
2133
|
+
return searchMessagesLikeFast(ctx, db, query, providers, models, repos, allRoles, limit)
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
func searchMessagesFTSFast(ctx context.Context, db *sql.DB, query string, providers []string, models []string, repos []string, allRoles bool, limit int) ([]item, bool, error) {
|
|
2137
|
+
scanLimit := max(limit*6, limit)
|
|
2138
|
+
args := []any{toFTSQuery(query)}
|
|
2139
|
+
filters := messageFilters("m", providers, models, repos, allRoles, &args)
|
|
2140
|
+
filters = append(filters, rootSessionFilter("s"))
|
|
2141
|
+
filters = append(filters, displayableSessionFilter("s"))
|
|
2142
|
+
where := "message_fts MATCH ?"
|
|
2143
|
+
if len(filters) > 0 {
|
|
2144
|
+
where += " AND " + strings.Join(filters, " AND ")
|
|
2145
|
+
}
|
|
2146
|
+
if scanLimit > 0 {
|
|
2147
|
+
args = append(args, scanLimit)
|
|
2148
|
+
}
|
|
2149
|
+
limitSQL := ""
|
|
2150
|
+
if scanLimit > 0 {
|
|
2151
|
+
limitSQL = " LIMIT ?"
|
|
2152
|
+
}
|
|
2153
|
+
rows, err := db.QueryContext(ctx, `
|
|
2154
|
+
SELECT m.session_key, m.provider, `+displayTitleExpr("s")+`,
|
|
2155
|
+
COALESCE(m.timestamp, s.updated_at, s.started_at, ''),
|
|
2156
|
+
COALESCE(s.started_at, ''),
|
|
2157
|
+
COALESCE(s.updated_at, s.started_at, ''),
|
|
2158
|
+
COALESCE(s.cwd, ''), COALESCE(s.message_count, 0),
|
|
2159
|
+
COALESCE((SELECT GROUP_CONCAT(model) FROM (
|
|
2160
|
+
SELECT DISTINCT sm.model FROM session_models sm
|
|
2161
|
+
WHERE sm.session_key = m.session_key
|
|
2162
|
+
ORDER BY sm.model
|
|
2163
|
+
)), ''),
|
|
2164
|
+
COALESCE(snippet(message_fts, 0, '[', ']', '...', 18), m.text),
|
|
2165
|
+
m.ordinal
|
|
2166
|
+
FROM message_fts
|
|
2167
|
+
JOIN messages m ON m.message_key = message_fts.message_key
|
|
2168
|
+
JOIN sessions s ON s.session_key = m.session_key
|
|
2169
|
+
WHERE `+where+limitSQL+`
|
|
2170
|
+
`, args...)
|
|
2171
|
+
if err != nil {
|
|
2172
|
+
return nil, false, err
|
|
2173
|
+
}
|
|
2174
|
+
defer rows.Close()
|
|
2175
|
+
items, err := scanItems(rows, true)
|
|
2176
|
+
if err != nil {
|
|
2177
|
+
return nil, false, err
|
|
2178
|
+
}
|
|
2179
|
+
return finalizeTUIItems(items, limit, scanLimit)
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
func searchMessagesLikeFast(ctx context.Context, db *sql.DB, query string, providers []string, models []string, repos []string, allRoles bool, limit int) ([]item, bool, error) {
|
|
2183
|
+
scanLimit := max(limit*6, limit)
|
|
2184
|
+
args := []any{"%" + query + "%"}
|
|
2185
|
+
filters := []string{"m.text LIKE ?"}
|
|
2186
|
+
filters = append(filters, messageFilters("m", providers, models, repos, allRoles, &args)...)
|
|
2187
|
+
filters = append(filters, rootSessionFilter("s"))
|
|
2188
|
+
filters = append(filters, displayableSessionFilter("s"))
|
|
2189
|
+
if scanLimit > 0 {
|
|
2190
|
+
args = append(args, scanLimit)
|
|
2191
|
+
}
|
|
2192
|
+
limitSQL := ""
|
|
2193
|
+
if scanLimit > 0 {
|
|
2194
|
+
limitSQL = " LIMIT ?"
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
rows, err := db.QueryContext(ctx, `
|
|
2198
|
+
SELECT m.session_key, m.provider, `+displayTitleExpr("s")+`,
|
|
2199
|
+
COALESCE(m.timestamp, s.updated_at, s.started_at, ''),
|
|
2200
|
+
COALESCE(s.started_at, ''),
|
|
2201
|
+
COALESCE(s.updated_at, s.started_at, ''),
|
|
2202
|
+
COALESCE(s.cwd, ''), COALESCE(s.message_count, 0),
|
|
2203
|
+
COALESCE((SELECT GROUP_CONCAT(model) FROM (
|
|
2204
|
+
SELECT DISTINCT sm.model FROM session_models sm
|
|
2205
|
+
WHERE sm.session_key = m.session_key
|
|
2206
|
+
ORDER BY sm.model
|
|
2207
|
+
)), ''),
|
|
2208
|
+
m.text,
|
|
2209
|
+
m.ordinal
|
|
2210
|
+
FROM messages m
|
|
2211
|
+
JOIN sessions s ON s.session_key = m.session_key
|
|
2212
|
+
WHERE `+strings.Join(filters, " AND ")+limitSQL+`
|
|
2213
|
+
`, args...)
|
|
2214
|
+
if err != nil {
|
|
2215
|
+
return nil, false, err
|
|
2216
|
+
}
|
|
2217
|
+
defer rows.Close()
|
|
2218
|
+
items, err := scanItems(rows, true)
|
|
2219
|
+
if err != nil {
|
|
2220
|
+
return nil, false, err
|
|
2221
|
+
}
|
|
2222
|
+
return finalizeTUIItems(items, limit, scanLimit)
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
func finalizeTUIItems(items []item, limit int, scanLimit int) ([]item, bool, error) {
|
|
2226
|
+
deduped := dedupeItems(items)
|
|
2227
|
+
sortItemsByRecency(deduped)
|
|
2228
|
+
rows, truncated, err := limitItems(deduped, limit)
|
|
2229
|
+
if err != nil {
|
|
2230
|
+
return nil, false, err
|
|
2231
|
+
}
|
|
2232
|
+
return rows, truncated || (scanLimit > 0 && len(items) >= scanLimit), nil
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2058
2235
|
func searchMessagesFTS(ctx context.Context, db *sql.DB, query string, providers []string, models []string, repos []string, allRoles bool, limit int) ([]item, bool, error) {
|
|
2059
2236
|
args := []any{toFTSQuery(query)}
|
|
2060
2237
|
filters := messageFilters("m", providers, models, repos, allRoles, &args)
|
|
@@ -2175,6 +2352,20 @@ func dedupeItems(items []item) []item {
|
|
|
2175
2352
|
return deduped
|
|
2176
2353
|
}
|
|
2177
2354
|
|
|
2355
|
+
func sortItemsByRecency(items []item) {
|
|
2356
|
+
sort.SliceStable(items, func(i int, j int) bool {
|
|
2357
|
+
left := items[i].UpdatedAt
|
|
2358
|
+
if left == "" {
|
|
2359
|
+
left = items[i].Timestamp
|
|
2360
|
+
}
|
|
2361
|
+
right := items[j].UpdatedAt
|
|
2362
|
+
if right == "" {
|
|
2363
|
+
right = items[j].Timestamp
|
|
2364
|
+
}
|
|
2365
|
+
return left > right
|
|
2366
|
+
})
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2178
2369
|
func limitItems(items []item, limit int) ([]item, bool, error) {
|
|
2179
2370
|
if limit <= 0 || len(items) <= limit {
|
|
2180
2371
|
return items, false, nil
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|