Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/ucode/skills_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@ class SkillRef:
``name:`` an agent reads from the bundle's SKILL.md frontmatter, so it names
the on-disk directory. Finalize does not require the securable and bundle name
to match, so a skill created under a securable that differs from its
frontmatter carries both.
frontmatter carries both. ``description`` is the skill's UC description, used only
to preview a skill in the interactive picker.
"""

catalog: str
schema: str
securable_name: str
bundle_name: str
description: str | None = None

@property
def fqn(self) -> str:
Expand Down Expand Up @@ -110,7 +112,11 @@ def _skill_ref(skill: dict) -> SkillRef | None:
return None
catalog, schema, securable_name = parts
return SkillRef(
catalog=catalog, schema=schema, securable_name=securable_name, bundle_name=bundle_name
catalog=catalog,
schema=schema,
securable_name=securable_name,
bundle_name=bundle_name,
description=_non_empty_str(skill.get("description")),
)


Expand Down Expand Up @@ -569,10 +575,13 @@ def _skill_download_choice(ref: SkillRef, roots: list[Path]) -> questionary.Choi
"""Picker row for one skill: value is its FQN, title flags an on-disk bundle.

On-disk skills stay selectable, since re-downloading is a legitimate update and
the existing overwrite prompt confirms it.
the existing overwrite prompt confirms it. The detail footer previews the
description behind a bold bundle-name label (the row itself shows the FQN, so the
bundle name is the one identifier not otherwise on screen).
"""
on_disk = " (on disk)" if existing_skill_on_disk(roots, ref.bundle_name) else ""
return questionary.Choice(title=f"{ref.fqn}{on_disk}", value=ref.fqn)
description = f"{ref.bundle_name}: {ref.description}" if ref.description else None
return questionary.Choice(title=f"{ref.fqn}{on_disk}", value=ref.fqn, description=description)


def _skills_download_background_loader(
Expand Down Expand Up @@ -601,6 +610,7 @@ def prompt_for_skill_download_choices(
style=picker_style(),
background_loader=background_loader,
loading_noun="skills",
show_description=True,
).ask()
if selection is None:
return None
Expand Down
68 changes: 67 additions & 1 deletion src/ucode/ui/interactive_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@

PICKER_VISIBLE_ROWS = 10

# Cap the highlighted-row description preview so a long description stays within the
# footer instead of dominating the screen.
_DESCRIPTION_PREVIEW_CHARS = 240
# Left margin for the description footer, applied to wrapped lines too (see get_line_prefix).
_DESCRIPTION_INDENT = " "


def _description_preview(description: str) -> str:
"""``description`` truncated to the footer budget, with an ellipsis when clipped."""
if len(description) <= _DESCRIPTION_PREVIEW_CHARS:
return description
return description[: _DESCRIPTION_PREVIEW_CHARS - 1].rstrip() + "…"


def _description_footer_tokens(description: str) -> list[tuple[str, str]]:
"""Footer tokens for the highlighted row's description.

A caller emphasizes a leading label by formatting the description as ``"label: text"``:
the ``label:`` renders bold and the rest as the truncated preview. A description with no
``": "`` renders entirely as the preview."""
label, sep, body = description.partition(": ")
if not sep:
return [("class:instruction", _description_preview(description))]
return [("bold", f"{label}{sep}"), ("class:instruction", _description_preview(body))]


class _Back:
"""Sentinel type: a wizard step returns the `_BACK` instance when the user
Expand Down Expand Up @@ -101,14 +126,19 @@ def scrolling_checkbox(
allow_back: bool = False,
background_loader: Callable[[Callable[[list[questionary.Choice]], None]], None] | None = None,
loading_noun: str = "MCP services",
show_description: bool = False,
) -> Question:
"""Multi-select checkbox picker.

``background_loader``, if given, streams more choices in after the picker is already
on screen: it's run on a daemon thread and handed an ``append(choices)`` callback that
adds rows (deduped by value) and repaints, so the picker opens instantly on whatever
``choices`` are ready and fills in the rest without blocking. A footer shows a live
"loading more {loading_noun}…" count while it runs."""
"loading more {loading_noun}…" count while it runs.

``show_description`` adds a footer previewing the highlighted row's ``Choice.description``.
It's a separate window rather than questionary's inline ``show_description`` because the
choices window is sized to the row count, so an inline line would be clipped."""
merged_style = merge_styles_default(
[
questionary.Style([("bottom-toolbar", "noreverse")]),
Expand Down Expand Up @@ -160,6 +190,26 @@ def loading_tokens() -> list[tuple[str, str]]:
def has_search_string() -> bool:
return control.get_search_string_tokens() is not None

def pointed_description() -> str | None:
if not (show_description and control.filtered_choices):
return None
try:
pointed = control.get_pointed_at()
except IndexError:
return None
description = getattr(pointed, "description", None)
return description if isinstance(description, str) and description else None

def description_tokens() -> list[tuple[str, str]]:
description = pointed_description()
if description is None:
return []
return _description_footer_tokens(description)

@Condition
def has_description() -> bool:
return pointed_description() is not None

validation_prompt: PromptSession = PromptSession(bottom_toolbar=lambda: control.error_message)
# Render the prompt as a fixed 1-row window rather than a PromptSession
# container: the latter expands to fill the terminal height, which in a tall
Expand Down Expand Up @@ -210,6 +260,22 @@ def has_search_string() -> bool:
validation_prompt.layout.container,
filter=Condition(lambda: control.error_message is not None),
),
# Pinned at the bottom, one blank line below the other footers, so the
# highlighted row's description reads as a separate detail pane.
ConditionalContainer(
HSplit(
[
Window(height=Dimension.exact(1)),
Window(
height=Dimension.exact(2),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 (UX): the description footer window is fixed at Dimension.exact(2). A UC description containing embedded newlines will have its later lines clipped even when it's well under the 240-char budget. UC descriptions are usually single-line, so minor — but consider collapsing whitespace/newlines into spaces before preview so the 2-line budget is spent on content rather than a hard-wrapped first line.

content=FormattedTextControl(description_tokens),
wrap_lines=True,
get_line_prefix=lambda line, wrap: _DESCRIPTION_INDENT,
),
]
),
filter=has_description & ~IsDone(),
),
]
)
)
Expand Down
39 changes: 38 additions & 1 deletion tests/test_interactive_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@

import questionary

from ucode.ui.interactive_picker import StreamingInquirerControl, merge_new_choices
from ucode.ui.interactive_picker import (
_DESCRIPTION_PREVIEW_CHARS,
StreamingInquirerControl,
_description_footer_tokens,
_description_preview,
merge_new_choices,
)


def test_merge_new_choices_dedupes_by_value():
Expand Down Expand Up @@ -32,3 +38,34 @@ def test_tolerates_all_disabled_choices(self):
control = StreamingInquirerControl([disabled], pointer="›", show_description=False)
assert control.is_selection_valid() is True
control._get_choice_tokens() # must not raise


class TestDescriptionPreview:
def test_short_description_is_unchanged(self):
assert _description_preview("Routes tickets.") == "Routes tickets."

def test_at_the_limit_is_unchanged(self):
text = "x" * _DESCRIPTION_PREVIEW_CHARS
assert _description_preview(text) == text

def test_long_description_is_clipped_with_an_ellipsis(self):
preview = _description_preview("y" * (_DESCRIPTION_PREVIEW_CHARS + 50))
assert len(preview) == _DESCRIPTION_PREVIEW_CHARS
assert preview.endswith("…")


class TestDescriptionFooterTokens:
def test_leading_label_is_bold_and_body_follows(self):
tokens = _description_footer_tokens("triage: Routes tickets.")
assert tokens == [("bold", "triage: "), ("class:instruction", "Routes tickets.")]

def test_without_a_label_the_whole_string_is_the_body(self):
assert _description_footer_tokens("Routes tickets.") == [
("class:instruction", "Routes tickets.")
]

def test_only_the_body_is_truncated(self):
long_body = "z" * (_DESCRIPTION_PREVIEW_CHARS + 10)
label_token, body_token = _description_footer_tokens(f"triage: {long_body}")
assert label_token == ("bold", "triage: ")
assert body_token == ("class:instruction", _description_preview(long_body))
42 changes: 38 additions & 4 deletions tests/test_skills_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ def ref(
*,
catalog: str = "main",
schema: str = "default",
description: str | None = None,
) -> SkillRef:
"""A SkillRef whose two names match unless a differing bundle name is given."""
return SkillRef(
catalog=catalog,
schema=schema,
securable_name=securable_name,
bundle_name=bundle_name or securable_name,
description=description,
)


Expand Down Expand Up @@ -57,6 +59,23 @@ def test_keeps_finalized_skills_only(self, monkeypatch):
assert reason is None
assert refs == [ref("pii-handling"), ref("triage")]

def test_carries_description_when_present(self, monkeypatch):
payload = {
"skills": [
{
"name": "skills/main.default.triage",
"bundle_name": "triage",
"finalize_time": "2026-06-26T05:58:25Z",
"description": "Routes tickets by severity.",
}
]
}
monkeypatch.setattr(sd, "_http_get_json", lambda url, token, timeout=30: (payload, None))

refs, _ = sd.list_schema_skills(WS, "token", "main", "default")

assert refs == [ref("triage", description="Routes tickets by severity.")]

def test_keeps_both_names_when_bundle_differs_from_securable(self, monkeypatch):
# bundle_name comes from the bundle's SKILL.md frontmatter, so it can
# differ from the securable it was created under.
Expand Down Expand Up @@ -999,18 +1018,32 @@ def test_empty_walk_reports_no_skills(self, monkeypatch):


class TestSkillDownloadPicker:
def test_choice_value_is_fqn_and_flags_on_disk(self, tmp_path):
def test_choice_value_is_fqn_flags_on_disk_and_carries_description(self, tmp_path):
roots = skill_dir_roots(str(tmp_path))

fresh = sd._skill_download_choice(ref("triage"), roots)
fresh = sd._skill_download_choice(ref("triage", description="Routes tickets."), roots)
assert fresh.value == "main.default.triage"
assert "(on disk)" not in fresh.title
assert fresh.description == "triage: Routes tickets."

write_skill(roots, ref("triage"), {"SKILL.md": b"x"})
existing = sd._skill_download_choice(ref("triage"), roots)
assert existing.value == "main.default.triage"
assert "(on disk)" in existing.title

def test_choice_description_labels_by_bundle_name_not_securable(self, tmp_path):
roots = skill_dir_roots(str(tmp_path))
diverging = ref("task-prioritizer", "task-triage", description="Ranks work.")

choice = sd._skill_download_choice(diverging, roots)

assert choice.description == "task-triage: Ranks work."

def test_choice_without_description_has_no_footer_text(self, tmp_path):
roots = skill_dir_roots(str(tmp_path))

assert sd._skill_download_choice(ref("triage"), roots).description is None

def test_background_loader_streams_the_walk_in_as_choices(self, tmp_path, monkeypatch):
roots = skill_dir_roots(str(tmp_path))
captured = {}
Expand All @@ -1033,8 +1066,8 @@ def test_prompt_returns_selected_fqns(self, tmp_path, monkeypatch):
loader = lambda append: None # noqa: E731
captured = {}

def fake_checkbox(message, *, choices, instruction, style, background_loader, loading_noun):
captured.update(loading_noun=loading_noun, background_loader=background_loader)
def fake_checkbox(message, *, choices, instruction, style, background_loader, **kwargs):
captured.update(background_loader=background_loader, **kwargs)
return _FakePrompt(["main.default.triage", "ml.prod.scoring"])

monkeypatch.setattr(sd, "scrolling_checkbox", fake_checkbox)
Expand All @@ -1044,6 +1077,7 @@ def fake_checkbox(message, *, choices, instruction, style, background_loader, lo
"ml.prod.scoring",
]
assert captured["loading_noun"] == "skills"
assert captured["show_description"] is True
assert captured["background_loader"] is loader

def test_prompt_returns_none_on_cancel(self, tmp_path, monkeypatch):
Expand Down
Loading
Loading