comate-cli 0.5.5__tar.gz → 0.5.7__tar.gz
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.
- {comate_cli-0.5.5 → comate_cli-0.5.7}/PKG-INFO +1 -1
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/event_renderer.py +1 -1
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/selection_menu.py +17 -13
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui.py +115 -8
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/commands.py +82 -13
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/key_bindings.py +58 -47
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +1 -1
- {comate_cli-0.5.5 → comate_cli-0.5.7}/pyproject.toml +1 -1
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_event_renderer_streaming.py +10 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_selection_menu.py +34 -0
- comate_cli-0.5.7/tests/test_skills_slash_command.py +386 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_tui_esc_queue.py +40 -1
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_tui_queue_sdk_source.py +68 -1
- comate_cli-0.5.7/tests/test_tui_team_messages.py +121 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/uv.lock +2 -2
- comate_cli-0.5.5/tests/test_skills_slash_command.py +0 -205
- {comate_cli-0.5.5 → comate_cli-0.5.7}/.gitignore +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/README.md +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/__init__.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/__main__.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/main.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/app.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/rewind_store.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/session_view.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/docs/superpowers/plans/2026-04-03-phrase-shuffle.md +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/docs/superpowers/specs/2026-04-01-conditional-diff-subtitle-design.md +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/docs/superpowers/specs/2026-04-03-phrase-shuffle-design.md +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/conftest.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_context_command.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_format_error.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_handle_error.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_history_printer.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_history_sync.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_input_history.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_logo.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_main_args.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_preflight.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_question_view.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_rewind_store.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_status_bar.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_task_poll.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_tool_view.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.5.5 → comate_cli-0.5.7}/tests/test_update_check.py +0 -0
|
@@ -1647,7 +1647,7 @@ class EventRenderer:
|
|
|
1647
1647
|
return (True, None)
|
|
1648
1648
|
if reason == "waiting_for_plan_approval":
|
|
1649
1649
|
return (False, None)
|
|
1650
|
-
if reason == "interrupted":
|
|
1650
|
+
if reason == "interrupted" and "error" not in event.metadata:
|
|
1651
1651
|
self._append_history_entry(
|
|
1652
1652
|
HistoryEntry(entry_type="system", text="Current task interrupted.", severity="warning")
|
|
1653
1653
|
)
|
|
@@ -288,14 +288,13 @@ class SelectionMenuUI:
|
|
|
288
288
|
desc_style = "class:selection.description.selected" if is_selected else "class:selection.description"
|
|
289
289
|
|
|
290
290
|
if self._state.layout_mode == "inline":
|
|
291
|
-
fragments.
|
|
292
|
-
(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
),
|
|
291
|
+
fragments.extend(
|
|
292
|
+
self._inline_option_fragments(
|
|
293
|
+
option=option,
|
|
294
|
+
cursor=cursor,
|
|
295
|
+
marker=marker,
|
|
296
|
+
line_style=line_style,
|
|
297
|
+
desc_style=desc_style,
|
|
299
298
|
)
|
|
300
299
|
)
|
|
301
300
|
fragments.append(("", "\n"))
|
|
@@ -331,13 +330,15 @@ class SelectionMenuUI:
|
|
|
331
330
|
)
|
|
332
331
|
return fragments
|
|
333
332
|
|
|
334
|
-
def
|
|
333
|
+
def _inline_option_fragments(
|
|
335
334
|
self,
|
|
336
335
|
*,
|
|
337
336
|
option: SelectionOption,
|
|
338
337
|
cursor: str,
|
|
339
338
|
marker: str,
|
|
340
|
-
|
|
339
|
+
line_style: str,
|
|
340
|
+
desc_style: str,
|
|
341
|
+
) -> list[tuple[str, str]]:
|
|
341
342
|
total_width = max(20, int(self._state.inline_total_width or 100))
|
|
342
343
|
prefix = f" {cursor} {marker} "
|
|
343
344
|
prefix_width = get_cwidth(prefix)
|
|
@@ -357,13 +358,16 @@ class SelectionMenuUI:
|
|
|
357
358
|
name_padding = " " * max(0, name_width - get_cwidth(name_text))
|
|
358
359
|
|
|
359
360
|
if desc_width <= 0:
|
|
360
|
-
return fit_single_line(f"{prefix}{name_text}", total_width)
|
|
361
|
+
return [(line_style, fit_single_line(f"{prefix}{name_text}", total_width))]
|
|
361
362
|
|
|
362
363
|
description = option.description.strip()
|
|
363
364
|
desc_text = fit_single_line(description, desc_width) if description else ""
|
|
364
365
|
if desc_text:
|
|
365
|
-
return
|
|
366
|
-
|
|
366
|
+
return [
|
|
367
|
+
(line_style, f"{prefix}{name_text}{name_padding}"),
|
|
368
|
+
(desc_style, f"{' ' * gap_width}{desc_text}"),
|
|
369
|
+
]
|
|
370
|
+
return [(line_style, f"{prefix}{name_text}{name_padding}")]
|
|
367
371
|
|
|
368
372
|
def _normalized_page_size(self) -> int:
|
|
369
373
|
options_count = len(self._state.options)
|
|
@@ -179,9 +179,13 @@ class TerminalAgentTUI(
|
|
|
179
179
|
from prompt_toolkit.application import get_app
|
|
180
180
|
from prompt_toolkit.application.dummy import DummyApplication
|
|
181
181
|
app = get_app()
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
if isinstance(app, DummyApplication):
|
|
183
|
+
_write()
|
|
184
|
+
else:
|
|
185
|
+
# run_in_terminal() 自身已经会调度 Future;不能再交给
|
|
186
|
+
# create_background_task() 二次包装,否则会触发重复执行竞态。
|
|
187
|
+
run_in_terminal(_write, in_executor=False)
|
|
188
|
+
return
|
|
185
189
|
except Exception:
|
|
186
190
|
pass
|
|
187
191
|
_write()
|
|
@@ -222,6 +226,7 @@ class TerminalAgentTUI(
|
|
|
222
226
|
self._renderer._show_thinking_cb = lambda: self._show_thinking
|
|
223
227
|
|
|
224
228
|
self._custom_slash_commands: dict[str, CustomSlashCommand] = {}
|
|
229
|
+
self._skill_slash_command_names: set[str] = set()
|
|
225
230
|
self._slash_registry = SlashCommandRegistry()
|
|
226
231
|
self._build_slash_registry()
|
|
227
232
|
self._slash_completer = SlashCommandCompleter(
|
|
@@ -237,6 +242,7 @@ class TerminalAgentTUI(
|
|
|
237
242
|
deduplicate=True,
|
|
238
243
|
)
|
|
239
244
|
)
|
|
245
|
+
self._sync_skill_slash_commands()
|
|
240
246
|
self._loading_frame = 0
|
|
241
247
|
self._fallback_loading_phrase = (
|
|
242
248
|
random.choice(DEFAULT_STATUS_PHRASES)
|
|
@@ -719,6 +725,56 @@ class TerminalAgentTUI(
|
|
|
719
725
|
argument_hint=command.argument_hint,
|
|
720
726
|
)
|
|
721
727
|
|
|
728
|
+
def _sync_skill_slash_commands(self) -> None:
|
|
729
|
+
"""同步当前 runtime skills 到 slash command registry。"""
|
|
730
|
+
for name in list(getattr(self, "_skill_slash_command_names", set())):
|
|
731
|
+
self._slash_registry.unregister(name)
|
|
732
|
+
self._skill_slash_command_names = set()
|
|
733
|
+
|
|
734
|
+
agent = getattr(self._session, "_agent", None)
|
|
735
|
+
runtime_state = getattr(agent, "_runtime_state", None)
|
|
736
|
+
skills = list(getattr(runtime_state, "skills", []) or [])
|
|
737
|
+
seen_names: set[str] = set()
|
|
738
|
+
|
|
739
|
+
for skill in skills:
|
|
740
|
+
if not bool(getattr(skill, "user_invocable", True)):
|
|
741
|
+
continue
|
|
742
|
+
|
|
743
|
+
name = str(getattr(skill, "name", "")).strip()
|
|
744
|
+
if not name or name in seen_names:
|
|
745
|
+
continue
|
|
746
|
+
seen_names.add(name)
|
|
747
|
+
|
|
748
|
+
if self._slash_registry.resolve(name) is not None:
|
|
749
|
+
logger.warning(
|
|
750
|
+
"Skill '%s' conflicts with an existing slash command, skipping",
|
|
751
|
+
name,
|
|
752
|
+
)
|
|
753
|
+
continue
|
|
754
|
+
|
|
755
|
+
spec = SlashCommandSpec(
|
|
756
|
+
name=name,
|
|
757
|
+
description=str(getattr(skill, "description", "") or "").strip(),
|
|
758
|
+
execution_kind="hybrid",
|
|
759
|
+
argument_hint=getattr(skill, "argument_hint", None),
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
async def _handler(args: str, *, skill_name: str = name) -> None:
|
|
763
|
+
await self._slash_skill(skill_name=skill_name, args=args)
|
|
764
|
+
|
|
765
|
+
self._slash_registry.register(
|
|
766
|
+
spec=spec,
|
|
767
|
+
handler=_handler,
|
|
768
|
+
allow_when_busy=False,
|
|
769
|
+
source="skill",
|
|
770
|
+
argument_hint=spec.argument_hint,
|
|
771
|
+
)
|
|
772
|
+
self._skill_slash_command_names.add(name)
|
|
773
|
+
|
|
774
|
+
slash_completer = getattr(self, "_slash_completer", None)
|
|
775
|
+
if slash_completer is not None:
|
|
776
|
+
slash_completer.update_commands(self._slash_registry.command_specs())
|
|
777
|
+
|
|
722
778
|
def _resolve_slash_spec(self, name: str) -> SlashCommandSpec | None:
|
|
723
779
|
entry = self._slash_registry.resolve(name)
|
|
724
780
|
if entry is None:
|
|
@@ -904,6 +960,7 @@ class TerminalAgentTUI(
|
|
|
904
960
|
async for event in self._session.events():
|
|
905
961
|
if self._closing:
|
|
906
962
|
break
|
|
963
|
+
stop_error: Exception | None = None
|
|
907
964
|
self._maybe_cancel_tool_result_flash(event)
|
|
908
965
|
await self._animation_controller.on_event(event)
|
|
909
966
|
if isinstance(event, SessionInitEvent):
|
|
@@ -918,6 +975,36 @@ class TerminalAgentTUI(
|
|
|
918
975
|
str(message_id),
|
|
919
976
|
None,
|
|
920
977
|
)
|
|
978
|
+
if event.reason is QueueDirtyReason.CONSUMED:
|
|
979
|
+
consumed_human_displays: list[str] = []
|
|
980
|
+
for message_id, origin in zip(event.message_ids, event.origins):
|
|
981
|
+
if origin is not MessageOrigin.HUMAN:
|
|
982
|
+
continue
|
|
983
|
+
display_text = self._queued_display_by_message_id.pop(
|
|
984
|
+
str(message_id),
|
|
985
|
+
None,
|
|
986
|
+
)
|
|
987
|
+
if display_text:
|
|
988
|
+
consumed_human_displays.append(display_text)
|
|
989
|
+
if consumed_human_displays and not self._busy:
|
|
990
|
+
self._session.run_controller.clear()
|
|
991
|
+
self._interrupt_requested_at = None
|
|
992
|
+
self._waiting_for_input = False
|
|
993
|
+
self._pending_questions = None
|
|
994
|
+
self._pending_plan_approval = None
|
|
995
|
+
self._set_busy(True)
|
|
996
|
+
self._run_start_time = time.monotonic()
|
|
997
|
+
self._fallback_loading_phrase = (
|
|
998
|
+
random.choice(DEFAULT_STATUS_PHRASES)
|
|
999
|
+
if DEFAULT_STATUS_PHRASES
|
|
1000
|
+
else f"Thinking{ELLIPSIS}"
|
|
1001
|
+
)
|
|
1002
|
+
self._fallback_phrase_refresh_at = time.monotonic() + 3.0
|
|
1003
|
+
self._renderer.start_turn()
|
|
1004
|
+
await self._animation_controller.start()
|
|
1005
|
+
self._last_turn_user_preview = consumed_human_displays[-1]
|
|
1006
|
+
for display_text in consumed_human_displays:
|
|
1007
|
+
self._renderer.seed_user_message(display_text)
|
|
921
1008
|
self._refresh_layers()
|
|
922
1009
|
self._invalidate()
|
|
923
1010
|
if isinstance(event, QueuedMessageInjectedEvent):
|
|
@@ -953,6 +1040,14 @@ class TerminalAgentTUI(
|
|
|
953
1040
|
}
|
|
954
1041
|
if isinstance(event, StopEvent):
|
|
955
1042
|
stop_reason = str(event.reason or "")
|
|
1043
|
+
raw_stop_error = event.metadata.get("error")
|
|
1044
|
+
if raw_stop_error is not None:
|
|
1045
|
+
stop_error = (
|
|
1046
|
+
raw_stop_error
|
|
1047
|
+
if isinstance(raw_stop_error, Exception)
|
|
1048
|
+
else Exception(str(raw_stop_error))
|
|
1049
|
+
)
|
|
1050
|
+
stop_reason = "error"
|
|
956
1051
|
if event.reason == "shutdown_request":
|
|
957
1052
|
shutdown_recipients = event.metadata.get("shutdown_recipients", [])
|
|
958
1053
|
if shutdown_recipients:
|
|
@@ -968,6 +1063,15 @@ class TerminalAgentTUI(
|
|
|
968
1063
|
waiting_for_input = True
|
|
969
1064
|
if new_questions is not None:
|
|
970
1065
|
questions = new_questions
|
|
1066
|
+
if stop_error is not None:
|
|
1067
|
+
from comate_cli.terminal_agent.error_display import format_error
|
|
1068
|
+
|
|
1069
|
+
message, transient_summary, severity = format_error(stop_error)
|
|
1070
|
+
self._renderer.append_system_message(message, severity=severity)
|
|
1071
|
+
self._status_bar.show_transient(
|
|
1072
|
+
transient_summary,
|
|
1073
|
+
severity=severity,
|
|
1074
|
+
)
|
|
971
1075
|
self._refresh_layers()
|
|
972
1076
|
if isinstance(event, StopEvent):
|
|
973
1077
|
background_turn = self._background_turn_source is not None
|
|
@@ -1487,6 +1591,9 @@ class TerminalAgentTUI(
|
|
|
1487
1591
|
|
|
1488
1592
|
# Commands → SlashCommandRegistry with namespace prefix
|
|
1489
1593
|
for cmd in plugin.commands:
|
|
1594
|
+
if cmd.name in getattr(self, "_skill_slash_command_names", set()):
|
|
1595
|
+
self._slash_registry.unregister(cmd.name)
|
|
1596
|
+
self._skill_slash_command_names.discard(cmd.name)
|
|
1490
1597
|
spec = SlashCommandSpec(
|
|
1491
1598
|
name=cmd.name, # already namespaced: "plugin-name:cmd-name"
|
|
1492
1599
|
description=cmd.description,
|
|
@@ -1544,8 +1651,10 @@ class TerminalAgentTUI(
|
|
|
1544
1651
|
if any(p.agents for p in plugins):
|
|
1545
1652
|
rebuild_agent_tool(agent)
|
|
1546
1653
|
|
|
1654
|
+
if any(p.skills for p in plugins):
|
|
1655
|
+
self._sync_skill_slash_commands()
|
|
1547
1656
|
# Refresh tab-completion so new plugin commands appear
|
|
1548
|
-
|
|
1657
|
+
elif any(p.commands for p in plugins):
|
|
1549
1658
|
self._slash_completer.update_commands(self._slash_registry.command_specs())
|
|
1550
1659
|
|
|
1551
1660
|
def _clear_plugin_resources(self) -> None:
|
|
@@ -1583,10 +1692,6 @@ class TerminalAgentTUI(
|
|
|
1583
1692
|
self._custom_slash_commands.pop(cmd_name, None)
|
|
1584
1693
|
self._plugin_command_names = set()
|
|
1585
1694
|
|
|
1586
|
-
# Refresh tab-completion so removed commands disappear
|
|
1587
|
-
if had_commands:
|
|
1588
|
-
self._slash_completer.update_commands(self._slash_registry.command_specs())
|
|
1589
|
-
|
|
1590
1695
|
# Remove plugin MCP servers from frozen config
|
|
1591
1696
|
has_mcp = any(p.mcp_servers for p in loaded)
|
|
1592
1697
|
if has_mcp and agent.config.mcp_servers:
|
|
@@ -1610,3 +1715,5 @@ class TerminalAgentTUI(
|
|
|
1610
1715
|
rebuild_skill_tool(agent)
|
|
1611
1716
|
if had_agents:
|
|
1612
1717
|
rebuild_agent_tool(agent)
|
|
1718
|
+
if had_skills or had_commands:
|
|
1719
|
+
self._sync_skill_slash_commands()
|
|
@@ -4,14 +4,16 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import time
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
from prompt_toolkit.document import Document
|
|
11
11
|
|
|
12
12
|
from comate_agent_sdk.agent.llm_levels import ALL_LEVELS
|
|
13
|
+
from comate_agent_sdk.context import TokenCounter
|
|
13
14
|
from comate_agent_sdk.context.items import ItemType
|
|
14
15
|
from comate_agent_sdk.llm.messages import UserMessage
|
|
16
|
+
from comate_agent_sdk.skill.execution import invoke_skill_by_name
|
|
15
17
|
|
|
16
18
|
from comate_cli.terminal_agent.figures import (
|
|
17
19
|
CHECK_MARK,
|
|
@@ -30,6 +32,9 @@ from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode
|
|
|
30
32
|
|
|
31
33
|
logger = logging.getLogger(__name__)
|
|
32
34
|
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from comate_cli.terminal_agent.plugins.plugin_picker import PluginPickerAction
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
class CommandsMixin:
|
|
35
40
|
def _begin_llm_long_task(self) -> None:
|
|
@@ -165,7 +170,10 @@ class CommandsMixin:
|
|
|
165
170
|
terminal_width = None
|
|
166
171
|
|
|
167
172
|
def on_confirm(value: str) -> None:
|
|
168
|
-
self._prefill_user_input(
|
|
173
|
+
self._prefill_user_input(
|
|
174
|
+
f"/{value} ",
|
|
175
|
+
preserve_trailing_space=True,
|
|
176
|
+
)
|
|
169
177
|
self._refresh_layers()
|
|
170
178
|
|
|
171
179
|
def on_cancel() -> None:
|
|
@@ -739,30 +747,86 @@ class CommandsMixin:
|
|
|
739
747
|
|
|
740
748
|
def _active_session_skills(self) -> list[dict[str, str]]:
|
|
741
749
|
agent = getattr(self._session, "_agent", None)
|
|
742
|
-
|
|
743
|
-
loaded_skills = list(getattr(
|
|
750
|
+
runtime_state = getattr(agent, "_runtime_state", None)
|
|
751
|
+
loaded_skills = list(getattr(runtime_state, "skills", []) or [])
|
|
752
|
+
registered_names = set(getattr(self, "_skill_slash_command_names", set()))
|
|
753
|
+
plugin_namespaces = {
|
|
754
|
+
str(getattr(plugin, "namespace", "")).strip()
|
|
755
|
+
for plugin in getattr(self, "_loaded_plugins", []) or []
|
|
756
|
+
}
|
|
757
|
+
plugin_namespaces.discard("")
|
|
758
|
+
token_counter = TokenCounter()
|
|
744
759
|
seen_names: set[str] = set()
|
|
745
760
|
active: list[dict[str, str]] = []
|
|
746
761
|
|
|
747
762
|
for skill in loaded_skills:
|
|
748
|
-
if bool(getattr(skill, "disable_model_invocation", False)):
|
|
749
|
-
continue
|
|
750
|
-
|
|
751
763
|
name = str(getattr(skill, "name", "")).strip()
|
|
752
|
-
if not name or name in seen_names:
|
|
764
|
+
if not name or name in seen_names or name not in registered_names:
|
|
753
765
|
continue
|
|
754
766
|
|
|
755
|
-
description = str(getattr(skill, "description", "")).strip()
|
|
767
|
+
description = str(getattr(skill, "description", "") or "").strip()
|
|
768
|
+
when_to_use = str(getattr(skill, "when_to_use", "") or "").strip()
|
|
769
|
+
frontmatter_text = " ".join(
|
|
770
|
+
part for part in (name, description, when_to_use) if part
|
|
771
|
+
)
|
|
772
|
+
token_count = token_counter.count(frontmatter_text)
|
|
773
|
+
source = (
|
|
774
|
+
"plugin"
|
|
775
|
+
if any(name.startswith(f"{namespace}:") for namespace in plugin_namespaces)
|
|
776
|
+
else "user"
|
|
777
|
+
)
|
|
756
778
|
active.append(
|
|
757
779
|
{
|
|
758
780
|
"name": name,
|
|
759
|
-
"description":
|
|
781
|
+
"description": f"· {source} · ~{token_count} tok",
|
|
760
782
|
}
|
|
761
783
|
)
|
|
762
784
|
seen_names.add(name)
|
|
763
785
|
|
|
764
786
|
return active
|
|
765
787
|
|
|
788
|
+
async def _slash_skill(self, *, skill_name: str, args: str) -> None:
|
|
789
|
+
normalized_args = args.strip()
|
|
790
|
+
result = await invoke_skill_by_name(
|
|
791
|
+
self._session._agent,
|
|
792
|
+
skill_name,
|
|
793
|
+
args=normalized_args or None,
|
|
794
|
+
invocation_source="user_slash",
|
|
795
|
+
flush=False,
|
|
796
|
+
)
|
|
797
|
+
if not result.success:
|
|
798
|
+
self._renderer.append_system_message(result.message, severity="error")
|
|
799
|
+
return
|
|
800
|
+
self._session._agent._context.add_message(
|
|
801
|
+
UserMessage(
|
|
802
|
+
content=self._format_skill_slash_metadata(
|
|
803
|
+
skill_name=skill_name,
|
|
804
|
+
args=normalized_args,
|
|
805
|
+
),
|
|
806
|
+
is_meta=True,
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
self._session._agent._context.flush_pending_skill_items()
|
|
810
|
+
|
|
811
|
+
display_text = f"/{skill_name}"
|
|
812
|
+
if normalized_args:
|
|
813
|
+
display_text = f"{display_text} {normalized_args}"
|
|
814
|
+
|
|
815
|
+
model_prompt = "The requested skill has already been loaded. Continue using the injected skill instructions."
|
|
816
|
+
if normalized_args:
|
|
817
|
+
model_prompt = f"{model_prompt}\n\nArguments: {normalized_args}"
|
|
818
|
+
await self._submit_user_message(model_prompt, display_text=display_text)
|
|
819
|
+
|
|
820
|
+
@staticmethod
|
|
821
|
+
def _format_skill_slash_metadata(*, skill_name: str, args: str) -> str:
|
|
822
|
+
parts = [
|
|
823
|
+
f"<command-message>{skill_name}</command-message>",
|
|
824
|
+
f"<command-name>/{skill_name}</command-name>",
|
|
825
|
+
]
|
|
826
|
+
if args:
|
|
827
|
+
parts.append(f"<command-args>{args}</command-args>")
|
|
828
|
+
return "\n".join(parts)
|
|
829
|
+
|
|
766
830
|
async def _slash_custom(self, *, command_name: str, args: str) -> None:
|
|
767
831
|
command = self._custom_slash_commands.get(command_name)
|
|
768
832
|
if command is None:
|
|
@@ -1336,9 +1400,14 @@ class CommandsMixin:
|
|
|
1336
1400
|
return content_text
|
|
1337
1401
|
return fallback.strip()
|
|
1338
1402
|
|
|
1339
|
-
def _prefill_user_input(
|
|
1340
|
-
|
|
1341
|
-
|
|
1403
|
+
def _prefill_user_input(
|
|
1404
|
+
self,
|
|
1405
|
+
text: str,
|
|
1406
|
+
*,
|
|
1407
|
+
preserve_trailing_space: bool = False,
|
|
1408
|
+
) -> None:
|
|
1409
|
+
normalized = str(text).rstrip("\r\n") if preserve_trailing_space else str(text).strip()
|
|
1410
|
+
if not normalized.strip():
|
|
1342
1411
|
return
|
|
1343
1412
|
self._clear_paste_state()
|
|
1344
1413
|
self._last_input_len = len(normalized)
|
|
@@ -13,6 +13,61 @@ from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class KeyBindingsMixin:
|
|
16
|
+
def _pop_human_queue_to_input(self, buffer) -> bool: # type: ignore[no-untyped-def]
|
|
17
|
+
if str(getattr(buffer, "text", "") or "").strip():
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
peek_queue = getattr(self._session, "peek_queue", None)
|
|
21
|
+
queued_messages = tuple(peek_queue()) if callable(peek_queue) else ()
|
|
22
|
+
human_messages = [
|
|
23
|
+
message
|
|
24
|
+
for message in queued_messages
|
|
25
|
+
if getattr(message, "origin", None) == MessageOrigin.HUMAN
|
|
26
|
+
]
|
|
27
|
+
if not human_messages:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def _editable_text(message) -> str: # type: ignore[no-untyped-def]
|
|
31
|
+
content = getattr(message, "content", "")
|
|
32
|
+
if isinstance(content, str):
|
|
33
|
+
return content
|
|
34
|
+
if isinstance(content, list):
|
|
35
|
+
pieces: list[str] = []
|
|
36
|
+
for part in content:
|
|
37
|
+
if isinstance(part, dict):
|
|
38
|
+
part_type = str(part.get("type", ""))
|
|
39
|
+
if part_type == "text":
|
|
40
|
+
pieces.append(str(part.get("text", "")))
|
|
41
|
+
elif "image" in part_type:
|
|
42
|
+
pieces.append("[image]")
|
|
43
|
+
else:
|
|
44
|
+
pieces.append(str(part))
|
|
45
|
+
return "\n".join(piece for piece in pieces if piece)
|
|
46
|
+
return str(content)
|
|
47
|
+
|
|
48
|
+
text = "\n\n".join(
|
|
49
|
+
piece
|
|
50
|
+
for piece in (_editable_text(message) for message in human_messages)
|
|
51
|
+
if piece
|
|
52
|
+
)
|
|
53
|
+
buffer.text = text
|
|
54
|
+
buffer.cursor_position = len(text)
|
|
55
|
+
message_ids = tuple(
|
|
56
|
+
str(getattr(message, "message_id"))
|
|
57
|
+
for message in human_messages
|
|
58
|
+
)
|
|
59
|
+
cancel_many = getattr(self._session, "cancel_many", None)
|
|
60
|
+
if callable(cancel_many):
|
|
61
|
+
cancel_many(message_ids)
|
|
62
|
+
queued_display = getattr(self, "_queued_display_by_message_id", None)
|
|
63
|
+
if isinstance(queued_display, dict):
|
|
64
|
+
for message_id in message_ids:
|
|
65
|
+
queued_display.pop(message_id, None)
|
|
66
|
+
self._esc_press_count = 0
|
|
67
|
+
self._esc_last_pressed_at = time.monotonic()
|
|
68
|
+
self._invalidate()
|
|
69
|
+
return True
|
|
70
|
+
|
|
16
71
|
def _build_key_bindings(self) -> KeyBindings:
|
|
17
72
|
bindings = KeyBindings()
|
|
18
73
|
normal_mode = Condition(lambda: self._ui_mode == UIMode.NORMAL)
|
|
@@ -341,6 +396,8 @@ class KeyBindingsMixin:
|
|
|
341
396
|
if self._move_cursor_visual(buffer, backward=True):
|
|
342
397
|
self._invalidate()
|
|
343
398
|
return
|
|
399
|
+
if self._pop_human_queue_to_input(buffer):
|
|
400
|
+
return
|
|
344
401
|
if self._should_handle_history():
|
|
345
402
|
buffer.auto_up(count=1)
|
|
346
403
|
|
|
@@ -377,53 +434,7 @@ class KeyBindingsMixin:
|
|
|
377
434
|
self._esc_last_pressed_at = now
|
|
378
435
|
return
|
|
379
436
|
|
|
380
|
-
|
|
381
|
-
queued_messages = tuple(peek_queue()) if callable(peek_queue) else ()
|
|
382
|
-
human_messages = [
|
|
383
|
-
message
|
|
384
|
-
for message in queued_messages
|
|
385
|
-
if getattr(message, "origin", None) == MessageOrigin.HUMAN
|
|
386
|
-
]
|
|
387
|
-
if human_messages:
|
|
388
|
-
def _editable_text(message) -> str: # type: ignore[no-untyped-def]
|
|
389
|
-
content = getattr(message, "content", "")
|
|
390
|
-
if isinstance(content, str):
|
|
391
|
-
return content
|
|
392
|
-
if isinstance(content, list):
|
|
393
|
-
pieces: list[str] = []
|
|
394
|
-
for part in content:
|
|
395
|
-
if isinstance(part, dict):
|
|
396
|
-
part_type = str(part.get("type", ""))
|
|
397
|
-
if part_type == "text":
|
|
398
|
-
pieces.append(str(part.get("text", "")))
|
|
399
|
-
elif "image" in part_type:
|
|
400
|
-
pieces.append("[image]")
|
|
401
|
-
else:
|
|
402
|
-
pieces.append(str(part))
|
|
403
|
-
return "\n".join(piece for piece in pieces if piece)
|
|
404
|
-
return str(content)
|
|
405
|
-
|
|
406
|
-
text = "\n\n".join(
|
|
407
|
-
piece
|
|
408
|
-
for piece in (_editable_text(message) for message in human_messages)
|
|
409
|
-
if piece
|
|
410
|
-
)
|
|
411
|
-
buffer.text = text
|
|
412
|
-
buffer.cursor_position = len(text)
|
|
413
|
-
message_ids = tuple(
|
|
414
|
-
str(getattr(message, "message_id"))
|
|
415
|
-
for message in human_messages
|
|
416
|
-
)
|
|
417
|
-
cancel_many = getattr(self._session, "cancel_many", None)
|
|
418
|
-
if callable(cancel_many):
|
|
419
|
-
cancel_many(message_ids)
|
|
420
|
-
queued_display = getattr(self, "_queued_display_by_message_id", None)
|
|
421
|
-
if isinstance(queued_display, dict):
|
|
422
|
-
for message_id in message_ids:
|
|
423
|
-
queued_display.pop(message_id, None)
|
|
424
|
-
self._esc_press_count = 0
|
|
425
|
-
self._esc_last_pressed_at = now
|
|
426
|
-
self._invalidate()
|
|
437
|
+
if self._pop_human_queue_to_input(buffer):
|
|
427
438
|
return
|
|
428
439
|
|
|
429
440
|
if now - self._esc_last_pressed_at > self._esc_clear_window_seconds:
|
{comate_cli-0.5.5 → comate_cli-0.5.7}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py
RENAMED
|
@@ -6,7 +6,7 @@ from typing import Any, Callable, Literal
|
|
|
6
6
|
from comate_cli.terminal_agent.slash_commands import SlashCommandSpec
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
SlashCommandSource = Literal["builtin", "custom"]
|
|
9
|
+
SlashCommandSource = Literal["builtin", "custom", "skill"]
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@dataclass(frozen=True, slots=True)
|
|
@@ -40,6 +40,16 @@ class TestQ11Fallback(unittest.TestCase):
|
|
|
40
40
|
# 兜底走 _append_assistant_text → buffer 累
|
|
41
41
|
self.assertEqual(renderer._assistant_buffer, "legacy hello")
|
|
42
42
|
|
|
43
|
+
def test_provider_error_stop_does_not_add_interrupted_message(self) -> None:
|
|
44
|
+
renderer = EventRenderer()
|
|
45
|
+
renderer.start_turn()
|
|
46
|
+
renderer.handle_event(
|
|
47
|
+
StopEvent(reason="interrupted", metadata={"error": Exception("Connection refused")})
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
texts = [entry.text for entry in renderer.history_entries()]
|
|
51
|
+
self.assertNotIn("Current task interrupted.", texts)
|
|
52
|
+
|
|
43
53
|
def test_thinking_event_no_op_when_delta_received(self) -> None:
|
|
44
54
|
renderer = EventRenderer()
|
|
45
55
|
renderer.start_turn()
|
|
@@ -124,6 +124,40 @@ class TestSelectionMenu(unittest.TestCase):
|
|
|
124
124
|
self.assertIn("Helps users", lines[0])
|
|
125
125
|
self.assertNotIn(" Helps users", lines[0])
|
|
126
126
|
|
|
127
|
+
def test_inline_layout_styles_name_and_description_separately(self) -> None:
|
|
128
|
+
ui = SelectionMenuUI()
|
|
129
|
+
ok = ui.set_options(
|
|
130
|
+
title="menu",
|
|
131
|
+
options=[
|
|
132
|
+
{
|
|
133
|
+
"value": "find-skills",
|
|
134
|
+
"label": "find-skills",
|
|
135
|
+
"description": "· user · ~12 tok",
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
layout_mode="inline",
|
|
139
|
+
line_wrap=False,
|
|
140
|
+
inline_total_width=56,
|
|
141
|
+
inline_name_min_width=12,
|
|
142
|
+
inline_name_max_width=16,
|
|
143
|
+
)
|
|
144
|
+
self.assertTrue(ok)
|
|
145
|
+
|
|
146
|
+
fragments = ui._options_fragments()
|
|
147
|
+
self.assertTrue(
|
|
148
|
+
any(
|
|
149
|
+
style == "class:selection.option.selected" and "find-skills" in text
|
|
150
|
+
for style, text in fragments
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
self.assertTrue(
|
|
154
|
+
any(
|
|
155
|
+
style == "class:selection.description.selected"
|
|
156
|
+
and "· user · ~12 tok" in text
|
|
157
|
+
for style, text in fragments
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
127
161
|
def test_context_lines_render_before_options(self) -> None:
|
|
128
162
|
ui = SelectionMenuUI()
|
|
129
163
|
ok = ui.set_options(
|