@@ -301,6 +301,25 @@ def _badge_field_slots(app_cfg: FrameworkAppConfig) -> tuple[str, str]:
301301 return primary , secondary
302302
303303
304+ def _field (item : dict , key : str , default : str = "" ) -> str :
305+ """Read a domain field from a session dict — top-level first, falling
306+ back to ``extra_fields``. Returns a string; non-string values are
307+ coerced via ``str``.
308+
309+ Bare ``runtime.state.Session`` rounds app-specific values
310+ (``query``, ``environment``, ``summary``, ...) through
311+ ``extra_fields``. Typed state subclasses keep them at the top
312+ level. This helper lets the UI work against either shape without
313+ branching at every call site.
314+ """
315+ val = item .get (key )
316+ if not val :
317+ val = (item .get ("extra_fields" ) or {}).get (key )
318+ if val is None or val == "" :
319+ return default
320+ return val if isinstance (val , str ) else str (val )
321+
322+
304323def _resolve_field (item : dict , dotted_key : str ) -> str :
305324 """Resolve a dotted-path key against ``item.extra_fields`` (preferred)
306325 or the item dict itself.
@@ -334,12 +353,12 @@ def _render_session_row(sess: dict, store: SessionStore,
334353 """
335354 session_id = sess ["id" ]
336355 status = sess .get ("status" ) or ""
337- env = sess . get ( "environment" , "" )
356+ env = _field ( sess , "environment" , "" )
338357 age = _age (sess .get ("created_at" , "" ))
339358 sev_field , cat_field = _badge_field_slots (app_cfg )
340359 sev = (sess .get (sev_field ) or "" ) if sev_field else ""
341360 cat = (sess .get (cat_field ) or "" ) if cat_field else ""
342- summary = (sess . get ( "summary" ) or sess . get ( "query" ) or "" ).strip ()
361+ summary = (_field ( sess , "summary" ) or _field ( sess , "query" ) or "" ).strip ()
343362 toks = (sess .get ("token_usage" ) or {}).get ("total_tokens" , 0 )
344363 is_deleted = status == "deleted"
345364 is_selected = st .session_state .get ("selected_session" ) == session_id
@@ -793,8 +812,12 @@ def _render_summary_meta(sess: dict, app_cfg: FrameworkAppConfig) -> None:
793812 """Render the query, environment, tags, summary, configured detail
794813 fields, and prior-match callout.
795814 """
796- st .markdown (f"**Query:** { sess ['query' ]} " )
797- st .markdown (f"**Environment:** `{ sess ['environment' ]} `" )
815+ query = _field (sess , "query" )
816+ if query :
817+ st .markdown (f"**Query:** { query } " )
818+ env = _field (sess , "environment" )
819+ if env :
820+ st .markdown (f"**Environment:** `{ env } `" )
798821 # App-configured summary fields (e.g. submitter id / team / component).
799822 for field in app_cfg .ui .detail_fields :
800823 if field .section != "summary" :
@@ -804,8 +827,12 @@ def _render_summary_meta(sess: dict, app_cfg: FrameworkAppConfig) -> None:
804827 st .markdown (f"**{ field .label } :** { v } " )
805828 if sess .get ("tags" ):
806829 st .markdown ("**Tags:** " + " " .join (f"`{ t } `" for t in sess ["tags" ]))
807- if sess .get ("summary" ):
808- st .markdown (f"**Summary:** { sess ['summary' ]} " )
830+ summary = _field (sess , "summary" )
831+ if summary :
832+ st .markdown (f"**Summary:** { summary } " )
833+ escalated_to = _field (sess , "escalated_to" )
834+ if escalated_to :
835+ st .markdown (f"**Escalated to:** `{ escalated_to } `" )
809836 if sess .get ("matched_prior_inc" ):
810837 _render_prior_match (sess , app_cfg )
811838
@@ -1062,7 +1089,7 @@ def render_session_detail(store: SessionStore,
10621089 except FileNotFoundError :
10631090 return
10641091 status = sess .get ("status" ) or ""
1065- env = sess . get ( "environment" ) or ""
1092+ env = _field ( sess , "environment" ) or ""
10661093 toks = (sess .get ("token_usage" ) or {}).get ("total_tokens" , 0 )
10671094 header = (
10681095 f"`{ session_id } ` "
@@ -1349,12 +1376,34 @@ def _inject_global_css() -> None:
13491376 Without the ``[aria-expanded="true"]`` predicate the min-width sticks
13501377 around after the user collapses the sidebar, leaving a blank gap on
13511378 the left of the main content area.
1379+
1380+ On viewports narrower than the desktop breakpoint we (a) drop the
1381+ min-width pin so the sidebar fits the screen, (b) cap the sidebar
1382+ at the viewport width so the close affordance never falls off
1383+ screen, and (c) keep streamlit's collapse control reachable above
1384+ any sticky content the sidebar may render.
13521385 """
13531386 st .markdown (
13541387 f"""
13551388 <style>
1356- section[data-testid="stSidebar"][aria-expanded="true"] {{
1357- min-width: { _SIDEBAR_MIN_WIDTH_PX } px !important;
1389+ @media (min-width: 768px) {{
1390+ section[data-testid="stSidebar"][aria-expanded="true"] {{
1391+ min-width: { _SIDEBAR_MIN_WIDTH_PX } px !important;
1392+ }}
1393+ }}
1394+ @media (max-width: 767px) {{
1395+ section[data-testid="stSidebar"][aria-expanded="true"] {{
1396+ min-width: 0 !important;
1397+ width: 100vw !important;
1398+ max-width: 100vw !important;
1399+ }}
1400+ button[data-testid="stSidebarCollapseButton"],
1401+ button[data-testid="baseButton-headerNoPadding"][kind="headerNoPadding"] {{
1402+ position: fixed !important;
1403+ top: 0.5rem !important;
1404+ right: 0.5rem !important;
1405+ z-index: 999999 !important;
1406+ }}
13581407 }}
13591408 </style>
13601409 """ ,
0 commit comments