77from typing import Any
88
99from textual .app import ComposeResult
10- from rich .markup import escape
10+ from rich .text import Text
1111from textual .binding import Binding
1212from textual .containers import VerticalScroll
1313from textual .screen import ModalScreen
@@ -112,17 +112,32 @@ def __init__(
112112 self ._filtered_entries : list [QueryHistoryEntry ] = []
113113
114114 def _merge_entries (self ) -> list [QueryHistoryEntry ]:
115- """Merge history entries with starred-only queries."""
115+ """Merge history entries with starred-only queries.
116+
117+ Same-text rows collapse to a single most-recent entry so the row
118+ count matches the starring model (which is keyed by query text):
119+ starring one row never marks two visually-identical rows. In
120+ multi-connection (telescope) mode the dedupe key is
121+ (connection_name, query); in single-connection mode it's just
122+ the query.
123+ """
116124 result : list [QueryHistoryEntry ] = []
117125
118126 if self ._multi_connection :
119- history_queries_by_connection : dict [str , set [ str ]] = {}
127+ seen : dict [tuple [ str , str ], QueryHistoryEntry ] = {}
120128 for entry in self .history :
121129 starred_queries = self ._starred_by_connection .get (entry .connection_name , set ())
122130 entry .is_starred = entry .query .strip () in starred_queries
123131 entry .is_starred_only = False
124- history_queries_by_connection .setdefault (entry .connection_name , set ()).add (entry .query .strip ())
125- result .append (entry )
132+ key = (entry .connection_name , entry .query .strip ())
133+ existing = seen .get (key )
134+ if existing is None or entry .timestamp > existing .timestamp :
135+ seen [key ] = entry
136+ result .extend (seen .values ())
137+
138+ history_queries_by_connection : dict [str , set [str ]] = {}
139+ for (conn_name , query_text ) in seen :
140+ history_queries_by_connection .setdefault (conn_name , set ()).add (query_text )
126141
127142 for connection_name , starred_queries in self ._starred_by_connection .items ():
128143 history_queries = history_queries_by_connection .get (connection_name , set ())
@@ -137,15 +152,17 @@ def _merge_entries(self) -> list[QueryHistoryEntry]:
137152 )
138153 result .append (entry )
139154 else :
140- # Mark history entries that are starred
141- history_queries : set [str ] = set ()
155+ seen_single : dict [str , QueryHistoryEntry ] = {}
142156 for entry in self .history :
143157 entry .is_starred = entry .query .strip () in self .starred
144158 entry .is_starred_only = False
145- history_queries .add (entry .query .strip ())
146- result .append (entry )
159+ key = entry .query .strip ()
160+ existing = seen_single .get (key )
161+ if existing is None or entry .timestamp > existing .timestamp :
162+ seen_single [key ] = entry
163+ result .extend (seen_single .values ())
147164
148- # Add starred-only entries (queries that were starred but not in history )
165+ history_queries = set ( seen_single . keys () )
149166 for starred_query in self .starred :
150167 if starred_query not in history_queries :
151168 entry = QueryHistoryEntry (
@@ -221,7 +238,7 @@ def _update_preview(self, idx: int) -> None:
221238 entries = self ._get_display_entries ()
222239 if idx < len (entries ):
223240 preview = self .query_one ("#history-preview" , Static )
224- preview .update (escape (entries [idx ].query ))
241+ preview .update (Text (entries [idx ].query ))
225242
226243 def action_select (self ) -> None :
227244 entries = self ._get_display_entries ()
@@ -287,6 +304,17 @@ def action_cancel(self) -> None:
287304
288305 def on_key (self , event : Any ) -> None :
289306 if not self ._filter_active :
307+ if event .key in ("j" , "k" ):
308+ try :
309+ option_list = self .query_one ("#history-list" , OptionList )
310+ except Exception :
311+ return
312+ if event .key == "j" :
313+ option_list .action_cursor_down ()
314+ else :
315+ option_list .action_cursor_up ()
316+ event .prevent_default ()
317+ event .stop ()
290318 return
291319
292320 key = event .key
@@ -468,19 +496,25 @@ def _build_action_result(self, action: str, entry: QueryHistoryEntry) -> tuple[s
468496 return action , entry .query
469497
470498 def _build_option (self , entry : QueryHistoryEntry ) -> Option :
471- star = "[yellow]*[/] " if entry .is_starred else " "
472- # Collapse all whitespace to single spaces and strip
499+ # Build a Rich Text (not a markup string) so identifiers like
500+ # [dbo].[ServiceFeatures] aren't eaten by the markup parser:
501+ # rich.markup.escape only escapes "[a-z#/@…]" tags, so uppercase
502+ # bracket identifiers slip through and get consumed as fake tags.
473503 query_single_line = re .sub (r"\s+" , " " , entry .query ).strip ()
474504 max_len = 40 if self ._multi_connection else 55
475505 if len (query_single_line ) > max_len :
476506 query_preview = query_single_line [:max_len ] + "..."
477507 else :
478508 query_preview = query_single_line
479- query_preview = escape (query_preview )
509+
510+ text = Text ()
511+ if entry .is_starred :
512+ text .append ("* " , style = "yellow" )
513+ else :
514+ text .append (" " )
480515 if self ._multi_connection :
481516 conn_label = self ._truncate_label (self ._entry_connection_label (entry ), 24 )
482- conn_label = escape (conn_label )
483- option_id = self ._entry_option_id (entry )
484- if self ._multi_connection :
485- return Option (f"{ star } [dim]{ conn_label } [/]/{ query_preview } " , id = option_id )
486- return Option (f"{ star } { query_preview } " , id = option_id )
517+ text .append (conn_label , style = "dim" )
518+ text .append ("/" )
519+ text .append (query_preview )
520+ return Option (text , id = self ._entry_option_id (entry ))
0 commit comments