cctally 1.56.1 → 1.56.2

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.56.2] - 2026-06-27
9
+
10
+ ### Fixed
11
+ - Conversation viewer: the browse-rail model chip is no longer truncated mid-glyph by the adjacent cost/message-count — it previously clipped "opus" to "opu"/"op" when the fixed-width rail ran short of room; the rail now shows a single rigid primary-model chip plus a "+N" counter for any additional models, and the project/branch text ellipsizes first so the chip and the cost/message numbers always render in full (#243).
12
+ - Conversation viewer: a conversation whose main session ran one model but spawned subagents on another (e.g. opus driving haiku code-explorers) now shows the main-session model as the primary one — both the rail chip and the reader header now list the main model first ("opus, haiku") instead of alphabetically ("haiku, opus"), which previously made a haiku subagent look like the conversation's main model (#243).
13
+
8
14
  ## [1.56.1] - 2026-06-27
9
15
 
10
16
  ### Fixed
@@ -799,19 +799,51 @@ def _session_cost_map(conn, session_ids):
799
799
  return costs
800
800
 
801
801
 
802
+ def _models_main_first(rows):
803
+ """Order distinct non-null models so MAIN-SESSION models sort first, then
804
+ models that appear ONLY in subagent / sidechain threads; alphabetical within
805
+ each group. ``rows`` is an iterable of ``(model, source_path, is_sidechain)``.
806
+
807
+ A model is "main" iff it appears in at least one row that is neither a
808
+ subagent file (``_subagent_key(source_path) is None``) nor a sidechain
809
+ (``not is_sidechain``) — the exact negation of the subagent/sidechain
810
+ predicate used elsewhere in this module. A model used in BOTH the main
811
+ session AND a subagent counts as MAIN (it's a model the user actually drove
812
+ the session with). This is what makes the reader header + browse-rail chip
813
+ surface the real primary model (e.g. opus) first when the main session
814
+ spawned haiku subagents, rather than the alphabetical-first model — pre-fix
815
+ a plain ``sorted()`` ranked ``haiku`` ahead of ``opus`` (#243)."""
816
+ main, sub = set(), set()
817
+ for model, source_path, is_sidechain in rows:
818
+ if not model:
819
+ continue
820
+ if _subagent_key(source_path) is None and not is_sidechain:
821
+ main.add(model)
822
+ else:
823
+ sub.add(model)
824
+ return sorted(main) + sorted(sub - main)
825
+
826
+
802
827
  def _session_models_map(conn, session_ids):
803
- """{session_id: sorted distinct non-null models}."""
828
+ """{session_id: models ordered MAIN-session-first (subagent/sidechain-only
829
+ models last), alphabetical within each group — see _models_main_first}."""
804
830
  out = {sid: [] for sid in session_ids}
805
831
  if not session_ids:
806
832
  return out
807
833
  placeholders = ",".join("?" for _ in session_ids)
808
834
  sql = (
809
- "SELECT DISTINCT session_id, model FROM conversation_messages "
810
- "WHERE session_id IN (%s) AND model IS NOT NULL AND model != '' "
811
- "ORDER BY model" % placeholders
835
+ "SELECT DISTINCT session_id, model, source_path, is_sidechain "
836
+ "FROM conversation_messages "
837
+ "WHERE session_id IN (%s) AND model IS NOT NULL AND model != ''"
838
+ % placeholders
812
839
  )
813
- for sid, model in conn.execute(sql, list(session_ids)):
814
- out.setdefault(sid, []).append(model)
840
+ per_session = {}
841
+ for sid, model, source_path, is_sidechain in conn.execute(
842
+ sql, list(session_ids)):
843
+ per_session.setdefault(sid, []).append(
844
+ (model, source_path, is_sidechain))
845
+ for sid, rows in per_session.items():
846
+ out[sid] = _models_main_first(rows)
815
847
  return out
816
848
 
817
849
 
@@ -1764,7 +1796,9 @@ def get_conversation(conn, session_id, *, after=None, before=None, tail=False,
1764
1796
  "started_utc": logical[0][2],
1765
1797
  "last_activity_utc": logical[-1][2],
1766
1798
  "cost_usd": header_cost,
1767
- "models": sorted({r[6] for r in logical if r[6]}),
1799
+ # #243: main-session models first (r[6]=model, r[12]=source_path,
1800
+ # r[9]=is_sidechain) so opus outranks a haiku subagent, not sorted().
1801
+ "models": _models_main_first((r[6], r[12], r[9]) for r in logical),
1768
1802
  "last_anchor": last_anchor,
1769
1803
  "items": [],
1770
1804
  "subagent_meta": subagent_meta,
@@ -1814,7 +1848,9 @@ def get_conversation(conn, session_id, *, after=None, before=None, tail=False,
1814
1848
 
1815
1849
  first = logical[0]
1816
1850
  last = logical[-1]
1817
- models = sorted({r[6] for r in logical if r[6]})
1851
+ # #243: main-session models first (r[6]=model, r[12]=source_path,
1852
+ # r[9]=is_sidechain) so opus outranks a haiku subagent, not sorted().
1853
+ models = _models_main_first((r[6], r[12], r[9]) for r in logical)
1818
1854
  return {
1819
1855
  "session_id": session_id,
1820
1856
  "title": _title,