feat(messages): resolve @mentions and redesign thread display

Replace <@USERID> tokens with @handles using Slack handle (name) as
preferred identifier, falling back to display_name then real_name.

Redesign thread rendering with bullet glyphs (● top-level, ◆ thread
parent), ├/└ reply branches, horizontal rules between roots, and an
ellipsis row for skipped replies. Resolve orphan thread parents from
the message cache when possible; otherwise render an explicit
"orphaned thread — parent unknown" box.
This commit is contained in:
2026-04-16 16:55:52 -04:00
parent b9afd0d8da
commit b06604788e

View File

@@ -81,8 +81,12 @@ def resolve_user(state: dict, user_id: str) -> str:
if not isinstance(member, dict):
return user_id
# Slack Redux State Stores Name Fields at Top Level
name = member.get("display_name") or member.get("real_name") or member.get("name")
# Slack Handle (e.g. "evan") > Display Name > Real Name > fallback to ID
name = member.get("name") # Slack handle / username
if name and name != user_id:
return name
name = member.get("display_name") or member.get("real_name")
if name:
return name
@@ -96,6 +100,23 @@ def resolve_user(state: dict, user_id: str) -> str:
return user_id
def resolve_mentions(state: dict, text: str) -> str:
"""Replace Slack User Mentions (<@USERID>) With @DisplayNames"""
if not isinstance(text, str):
return text
members = state.get("members", {})
if not isinstance(members, dict):
return text
def _replace(match):
user_id = match.group(1)
resolved = resolve_user(state, user_id)
return f"@{resolved}"
return re.sub(r"<@([A-Z0-9]+)>", _replace, text)
def parse_since(value: str) -> datetime:
"""Parse a --since Value Into a Datetime
@@ -243,6 +264,7 @@ def cmd_messages(
user_id = msg.get("user", msg.get("bot_id", "?"))
user_name = resolve_user(state, user_id)
text = resolve_mentions(state, text)
subtype = msg.get("subtype", "")
# dfindexeddb Represents JS undefined as an Undefined object
@@ -280,39 +302,92 @@ def cmd_messages(
top_level.append(msg)
# Build Display List — Each Top-Level Entry Followed by Its Replies
# item = (msg | None, depth) — None msg means orphan thread header
# depth 0 = root (top-level message or orphan thread header)
# depth 1 = thread reply
display: list[tuple[dict | None, int]] = []
# item = (msg, depth)
# depth 0 = root (top-level message OR thread parent)
# depth 1 = thread reply (or ellipsis row)
# Every root carries `has_replies` so the renderer can pick glyphs.
display: list[tuple[dict, int]] = []
seen_parents: set[str] = {m["ts"] for m in top_level}
for msg in top_level:
replies = threads.get(msg["ts"], [])
msg["has_replies"] = bool(replies)
if replies:
msg["is_thread_parent"] = True
display.append((msg, 0))
for reply in threads.get(msg["ts"], []):
for reply in replies:
display.append((reply, 1))
# Collect Orphan Thread Groups — Replies Whose Parent Isn't Shown
# Collect Orphan Thread Groups — Replies Whose Parent Isn't a Top-Level Match
orphan_groups: list[tuple[str, list[dict]]] = []
for thread_ts, replies in threads.items():
if thread_ts not in seen_parents:
orphan_groups.append((thread_ts, replies))
# Sort Orphan Groups by Earliest Reply Timestamp
orphan_groups.sort(key=lambda g: g[1][0]["dt"])
# Append Each Orphan Group With a Header Placeholder
# Append Each Orphan Group — Resolve Parent From Cache When Possible.
# If parent isn't cached, emit a synthetic header with is_orphan=True so
# the renderer shows a consistent row + "parent not cached" body.
for thread_ts, replies in orphan_groups:
# Use First Reply's Channel and ID for the Header
header = {
"channel": replies[0]["channel"],
"channel_id": replies[0]["channel_id"],
cid = replies[0]["channel_id"]
ch_name = replies[0]["channel"]
channel_msgs_raw = messages_store.get(cid)
parent_raw = (
channel_msgs_raw.get(thread_ts)
if isinstance(channel_msgs_raw, dict) else None
)
if isinstance(parent_raw, dict) and isinstance(parent_raw.get("text"), str):
user_id = parent_raw.get("user", parent_raw.get("bot_id", "?"))
subtype = parent_raw.get("subtype", "")
if not isinstance(subtype, str):
subtype = ""
parent_msg = {
"channel": ch_name,
"channel_id": cid,
"ts": thread_ts,
"thread_ts": thread_ts,
"dt": ts_to_datetime(thread_ts),
"user": resolve_user(state, user_id),
"user_id": user_id,
"text": resolve_mentions(state, parent_raw.get("text", "")),
"subtype": subtype,
"is_thread_parent": True,
"has_replies": True,
}
display.append((None, 0)) # Placeholder for header
reply_count = parent_raw.get("reply_count")
shown = len(replies)
if isinstance(reply_count, int):
# Known: show ellipsis only if replies were actually skipped
skipped = reply_count - shown if reply_count > shown else 0
else:
# Unknown reply_count — signal ambiguous gap
skipped = None
else:
# Orphan — Parent Not in Local Cache
parent_msg = {
"channel": ch_name,
"channel_id": cid,
"ts": thread_ts,
"thread_ts": thread_ts,
"dt": ts_to_datetime(thread_ts),
"user": "???",
"user_id": "",
"text": "",
"subtype": "",
"is_thread_parent": True,
"is_orphan": True,
"has_replies": True,
}
# No reply_count available for orphans → generic ellipsis
skipped = None
display.append((parent_msg, 0))
# Suppress Ellipsis When We Know No Replies Were Skipped
if skipped != 0:
display.append(({"ellipsis": True, "skipped": skipped}, 1))
for reply in replies:
display.append((reply, 1))
# Patch the Placeholder With Header Info
display[-len(replies) - 1] = (header, 0)
# Print Last N Messages (Count depth=0 Entries Only)
if len(display) > 0:
@@ -330,52 +405,117 @@ def cmd_messages(
else:
visible = []
# ANSI Palette
dim = "\033[90m"
ts_color = "\033[90m"
ch_color = "\033[36m"
user_color = "\033[33m"
bullet_top = "\033[36m●\033[0m" # Top-level message (cyan dot)
bullet_thread = "\033[33m◆\033[0m" # Thread parent (yellow diamond)
reset = "\033[0m"
bar_str = f"\033[90m│\033[0m"
bar = f"{dim}{reset}" # Thread continuation gutter
branch_mid = f"{dim}{reset}" # Ellipsis branch
branch_end = f"{dim}{reset}" # Final reply branch
# Horizontal Rule Between Roots — Sized to Terminal Width (Clamped)
try:
term_width = os.get_terminal_size().columns
except OSError:
term_width = 78
rule_width = max(40, min(term_width, 100))
rule = f"{dim}{'' * rule_width}{reset}"
def wrap_text(text: str, prefix: str, limit: int = 500) -> None:
"""Print Message Body, Prefixing Every Line"""
for line in text[:limit].split("\n"):
print(f"{prefix}{line}")
prev_depth = None
for idx, (msg, depth) in enumerate(visible):
# Peek Ahead to See if Next Item Is Still a Thread Reply
next_is_reply = (idx + 1 < len(visible) and visible[idx + 1][1] > 0)
# Peek Ahead — Needed to Decide Between Mid vs End Reply Branch
next_is_reply = (
idx + 1 < len(visible) and visible[idx + 1][1] > 0
)
# Orphan Thread Header
if msg is not None and "dt" not in msg:
header_line = f"\033[90m↳ thread in {ch_color}#{msg['channel']}{reset}"
if domain and msg.get("channel_id") and msg.get("thread_ts"):
link = slack_url(domain, msg["channel_id"], msg["thread_ts"])
header_line += f" \033[90m{link}{reset}"
print(header_line)
# Horizontal Rule Before Every New Root
if depth == 0:
print(rule)
# Ellipsis Row — Between Thread Parent and Shown Replies
if msg.get("ellipsis"):
skipped = msg.get("skipped")
if skipped:
inner = f"[… {skipped} earlier repl{'y' if skipped == 1 else 'ies'}]"
else:
inner = "[… older replies]"
print(f"{branch_mid} {dim}{inner}{reset}")
# Continuation Bar Below Ellipsis if More Replies Follow
if next_is_reply:
print(bar)
continue
dt_str = msg["dt"].strftime("%Y-%m-%d %H:%M:%S")
subtype_tag = f" [{msg['subtype']}]" if msg["subtype"] else ""
bar = f"{bar_str} " if depth > 0 else ""
subtype_tag = f" [{msg['subtype']}]" if msg.get("subtype") else ""
# Build Slack Link for Top-Level Messages
if depth == 0:
# Root Row — Choose Bullet Based on Thread State
is_thread = msg.get("is_thread_parent", False)
bullet = bullet_thread if is_thread else bullet_top
link_suffix = ""
if domain and depth == 0:
if domain and msg.get("channel_id") and msg.get("ts"):
link = slack_url(domain, msg["channel_id"], msg["ts"])
link_suffix = f" {ts_color}{link}{reset}"
print(
f"{bar}{ts_color}{dt_str}{reset} "
f"{bullet} {ts_color}{dt_str}{reset} "
f"{ch_color}#{msg['channel']}{reset} "
f"{user_color}{msg['user']}{reset}{subtype_tag}{link_suffix}"
)
# Indent Message Text (Prefix Every Line for Multi-Line Messages)
text_prefix = f"{bar_str} " if depth > 0 else " "
for line in msg["text"][:500].split("\n"):
print(f"{text_prefix}{line}")
# Orphan Subtitle (Kept Below Header for Layout Consistency)
if msg.get("is_orphan"):
print(f"{bar} {dim}(orphan — parent not cached){reset}")
# Connecting Bar Between Thread Messages
# Body: Either the Orphan Placeholder Box or the Real Text
if msg.get("is_orphan"):
# ⚠ Renders as Double-Width in Most Terminals, So the
# top/bottom rules need to be 2 chars shorter than the
# printable width of the middle row.
box_top = f"{dim}{'' * 39}{reset}"
box_mid = f"{dim}{reset} \033[33m⚠ ORPHANED THREAD — PARENT UNKNOWN\033[0m {dim}{reset}"
box_bot = f"{dim}{'' * 39}{reset}"
print(f"{bar}")
print(f"{bar} {box_top}")
print(f"{bar} {box_mid}")
print(f"{bar} {box_bot}")
else:
# Top-Level and Resolved-Parent Bodies Both Indent 2 Spaces;
# thread parents use a gutter to signal replies follow.
body_prefix = f"{bar} " if msg.get("has_replies") else " "
wrap_text(msg["text"], body_prefix)
# Trailing Gutter Only When Replies Follow
if next_is_reply:
print(bar_str)
print(bar)
else:
print()
else:
# Reply Row — Use └ (Final) or ├ (Mid) Depending on Followups
branch = branch_mid if next_is_reply else branch_end
print(
f"{branch} {ts_color}{dt_str}{reset} "
f"{user_color}{msg['user']}{reset}{subtype_tag}"
)
# Mid Replies Keep the │ Gutter; Final Reply Indents Flat
text_prefix = f"{bar} " if next_is_reply else " "
wrap_text(msg["text"], text_prefix)
if next_is_reply:
print(bar)
else:
print()
prev_depth = depth
root_count = sum(1 for _, d in visible if d == 0)
total_roots = sum(1 for _, d in display if d == 0)