docs/cli-rpc-stdio-guide.md
355-工具需要审批时 server 发 request：
356-
357-```json
358:{"jsonrpc":"2.0","id":"srv-1","method":"permission.request","params":{"session_handle":"s-1","session_id":"sid-1","tool_name":"Bash","tool_input":{"command":"ls"},"tool_call_id":"tc-1","cwd":"/workspace/project","permission_mode":"default","reason":"ask","permission_suggestions":[],"policy_source":"rule","agent_id":null,"agent_type":null,"team_name":null}}
359-```
360-
361-`params` 是 SDK tool approval payload 外加 RPC `session_handle`，因此会同时包含 SDK `session_id` 和 RPC handle。
tests/statusline/test_picker_state.py
36-
37-
38-def test_space_toggles_current_field(state):
39:    assert state.draft.show_model_name is True
40-    state.handle_space()  # cursor at 0 = show_model_name
41-    assert state.draft.show_model_name is False
42-    state.handle_space()
37-
38-def test_space_toggles_current_field(state):
39-    assert state.draft.show_model_name is True
40:    state.handle_space()  # cursor at 0 = show_model_name
41-    assert state.draft.show_model_name is False
42-    state.handle_space()
43-    assert state.draft.show_model_name is True
38-def test_space_toggles_current_field(state):
39-    assert state.draft.show_model_name is True
40-    state.handle_space()  # cursor at 0 = show_model_name
41:    assert state.draft.show_model_name is False
42-    state.handle_space()
43-    assert state.draft.show_model_name is True
44-
40-    state.handle_space()  # cursor at 0 = show_model_name
41-    assert state.draft.show_model_name is False
42-    state.handle_space()
43:    assert state.draft.show_model_name is True
44-
45-
46-def test_space_does_not_affect_other_fields(state):
45-
46-def test_space_does_not_affect_other_fields(state):
47-    assert state.draft.show_token_usage is False
48:    state.handle_space()  # toggles show_model_name
49-    assert state.draft.show_token_usage is False
50-
51-
100-
101-    cfg = state.draft
102-    # 前 3 项默认 True → toggle 后 False
103:    assert cfg.show_model_name is False
104-    assert cfg.show_git_branch is False
105-    assert cfg.show_context_left is False
106-    # 其余默认 False → toggle 后 True
tests/statusline/test_model.py
7-def test_default_has_three_enabled():
8-    """默认：前 3 项 True，其余 False。"""
9-    cfg = StatuslineConfig.default()
10:    assert cfg.show_model_name is True
11-    assert cfg.show_git_branch is True
12-    assert cfg.show_context_left is True
13-    assert cfg.show_token_usage is False
25-    cfg = StatuslineConfig.default()
26-    updated = cfg.with_show_token_usage(True)
27-    assert updated.show_token_usage is True
28:    assert updated.show_model_name == cfg.show_model_name
29-    assert updated.show_git_branch == cfg.show_git_branch
30-
31-
tests/statusline/test_store.py
25-
26-def test_load_reads_namespaced_statusline_key(tmp_settings):
27-    tmp_settings.write_text(json.dumps({
28:        "statusline": {"show_model_name": True},
29-        "comate_cli": {
30-            "statusline": {
31-                "show_model_name": False,
28-        "statusline": {"show_model_name": True},
29-        "comate_cli": {
30-            "statusline": {
31:                "show_model_name": False,
32-                "show_token_usage": True,
33-                "show_session_cost": True,
34-            }
37-
38-    cfg = store.load()
39-
40:    assert cfg.show_model_name is False
41-    assert cfg.show_token_usage is True
42-    assert cfg.show_session_cost is True
43-    assert cfg.show_git_branch is True  # default preserved
45-
46-def test_load_falls_back_to_legacy_statusline_key(tmp_settings):
47-    tmp_settings.write_text(json.dumps({
48:        "statusline": {"show_model_name": False, "show_token_usage": True},
49-    }))
50-
51-    cfg = store.load()
50-
51-    cfg = store.load()
52-
53:    assert cfg.show_model_name is False
54-    assert cfg.show_token_usage is True
55-    assert cfg.show_git_branch is True
56-
57-
58-def test_load_ignores_non_bool_values(tmp_settings):
59-    tmp_settings.write_text(json.dumps({
60:        "statusline": {"show_model_name": "yes", "show_token_usage": 1},
61-    }))
62-    cfg = store.load()
63-    assert cfg.show_model_name is True  # falls back to default
60-        "statusline": {"show_model_name": "yes", "show_token_usage": 1},
61-    }))
62-    cfg = store.load()
63:    assert cfg.show_model_name is True  # falls back to default
64-    assert cfg.show_token_usage is False
65-
66-
81-        "comate_cli": {
82-            "statusline": {
83-                "show_goal": False,
84:                "show_model_name": False,
85-            }
86-        },
87-    }))
89-    cfg = store.load()
90-
91-    assert not hasattr(cfg, "show_goal")
92:    assert cfg.show_model_name is False
93-
94-
95-def test_save_writes_namespaced_statusline_and_removes_legacy_key(tmp_settings):
95-def test_save_writes_namespaced_statusline_and_removes_legacy_key(tmp_settings):
96-    tmp_settings.write_text(json.dumps({
97-        "model": "mid",
98:        "statusline": {"show_model_name": False},
99-        "comate_cli": {"config": {"token_cost": {"enabled": False}}},
100-    }))
101-    cfg = StatuslineConfig.default().with_show_token_usage(True).with_show_session_cost(True)
109-    assert data["comate_cli"]["config"] == {"token_cost": {"enabled": False}}
110-    assert data["comate_cli"]["statusline"]["show_token_usage"] is True
111-    assert data["comate_cli"]["statusline"]["show_session_cost"] is True
112:    assert data["comate_cli"]["statusline"]["show_model_name"] is True
113-    assert "show_goal" not in data["comate_cli"]["statusline"]
114-
115-
tests/test_preflight.py
317-            "true",
318-        )
319-        statusline = hydrated["comate_cli"]["statusline"]
320:        self.assertTrue(statusline["show_model_name"])
321-        self.assertTrue(statusline["show_context_left"])
322-        self.assertFalse(statusline["show_session_cost"])
323-        self.assertNotIn("show_goal", statusline)
348-        self.assertEqual(config["display"], {"thinking": False})
349-        self.assertEqual(config["model"], {"level": "mid"})
350-        self.assertTrue(hydrated["comate_cli"]["statusline"]["show_session_cost"])
351:        self.assertTrue(hydrated["comate_cli"]["statusline"]["show_model_name"])
352-        self.assertNotIn("show_goal", hydrated["comate_cli"]["statusline"])
353-        self.assertEqual(hydrated["model"], "mid")
354-        self.assertEqual(hydrated["memory_state"]["enabled"], True)
405-        hydrated = _hydrate_cli_default_settings(
406-            {
407-                "statusline": {
408:                    "show_model_name": False,
409-                    "show_session_cost": True,
410-                },
411-            }
412-        )
413-
414-        statusline = hydrated["comate_cli"]["statusline"]
415:        self.assertFalse(statusline["show_model_name"])
416-        self.assertTrue(statusline["show_session_cost"])
417-        self.assertTrue(statusline["show_context_left"])
418-        self.assertNotIn("statusline", hydrated)
851-        self.assertEqual(data["permissions"], {"allow": ["Bash"]})
852-        self.assertEqual(data["llm_levels"]["LOW"], "openai:gpt-4o-mini")
853-        self.assertEqual(data["comate_cli"]["config"]["token_cost"], {"enabled": False})
854:        self.assertEqual(data["comate_cli"]["statusline"]["show_model_name"], True)
855-        self.assertEqual(data["model"], "mid")
856-        self.assertEqual(data["memory_state"], {"enabled": True})
857-        self.assertEqual(
tests/test_custom_slash_commands.py
21-
22-
23-class TestCustomSlashCommandDiscovery(unittest.TestCase):
24:    def test_discovery_respects_scope_priority_and_namespace(self) -> None:
25-        with tempfile.TemporaryDirectory() as td:
26-            root = Path(td)
27-            project_root = root / "project"
comate_cli/terminal_agent/slash_commands.py
64-        aliases=("h",),
65-    ),
66-    SlashCommandSpec(
67:        name="model",
68-        description="Switch model level (LOW/MID/HIGH)",
69-        execution_kind="local",
70-        aliases=("m",),
278-        if parsed.args:
279-            return None
280-
281:        spec = self._resolve_spec(parsed.name)
282-        if spec is None:
283-            return None
284-        hint = str(spec.argument_hint or "").strip()
comate_cli/terminal_agent/selection_menu.py
506-    current_level: str,
507-    llm_levels: dict[str, Any] | None,
508-) -> tuple[str, list[dict[str, str]]]:
509:    def get_model_name(level: str) -> str:
510-        if llm_levels and level in llm_levels:
511-            llm = llm_levels[level]
512-            model = getattr(llm, "model", None)
518-        {
519-            "value": "LOW",
520-            "label": "LOW  - Fast & Cheap",
521:            "description": f"{get_model_name('LOW')}",
522-        },
523-        {
524-            "value": "MID",
523-        {
524-            "value": "MID",
525-            "label": "MID  - Balanced",
526:            "description": f"{get_model_name('MID')}",
527-        },
528-        {
529-            "value": "HIGH",
528-        {
529-            "value": "HIGH",
530-            "label": "HIGH - Best Quality",
531:            "description": f"{get_model_name('HIGH')}",
532-        },
533-    ]
534-
tests/test_goal_slash_command.py
113-
114-class TestGoalSlashCommand(unittest.IsolatedAsyncioTestCase):
115-    def test_builtin_goal_command_spec_exists(self) -> None:
116:        spec_by_name = {spec.name: spec for spec in SLASH_COMMAND_SPECS}
117-
118-        goal = spec_by_name["goal"]
119-        self.assertEqual(goal.execution_kind, "local")
115-    def test_builtin_goal_command_spec_exists(self) -> None:
116-        spec_by_name = {spec.name: spec for spec in SLASH_COMMAND_SPECS}
117-
118:        goal = spec_by_name["goal"]
119-        self.assertEqual(goal.execution_kind, "local")
120-        self.assertIn("goal", goal.description.lower())
121-        self.assertIsNotNone(goal.argument_hint)
tests/test_model_switch_command.py
24-
25-class _RecordingStatusBar:
26-    def __init__(self) -> None:
27:        self.model_names: list[str] = []
28-
29-    def set_model_name(self, model_name: str) -> None:
30-        self.model_names.append(model_name)
26-    def __init__(self) -> None:
27-        self.model_names: list[str] = []
28-
29:    def set_model_name(self, model_name: str) -> None:
30-        self.model_names.append(model_name)
31-
32-
27-        self.model_names: list[str] = []
28-
29-    def set_model_name(self, model_name: str) -> None:
30:        self.model_names.append(model_name)
31-
32-
33-class _RejectingSession:
68-        commands._slash_model("low")
69-
70-        self.assertEqual(commands._session.set_level_calls, ["LOW"])
71:        self.assertEqual(commands._status_bar.model_names, [])
72-        self.assertEqual(commands._invalidate_calls, 0)
73-        self.assertEqual(commands._selection_mode_entries, 0)
74-        self.assertEqual(commands._renderer.events[0], ("user", "/model low", "info"))
comate_cli/terminal_agent/statusline/model.py
8-class StatuslineConfig:
9-    """11 项 statusline 展示项开关。"""
10-
11:    show_model_name: bool = True
12-    show_git_branch: bool = True
13-    show_context_left: bool = True
14-    show_token_usage: bool = False
26-
27-    # ---- with_* mutators (返回新实例) ----
28-
29:    def with_show_model_name(self, v: bool) -> StatuslineConfig:
30-        return replace(self, show_model_name=v)
31-
32-    def with_show_git_branch(self, v: bool) -> StatuslineConfig:
27-    # ---- with_* mutators (返回新实例) ----
28-
29-    def with_show_model_name(self, v: bool) -> StatuslineConfig:
30:        return replace(self, show_model_name=v)
31-
32-    def with_show_git_branch(self, v: bool) -> StatuslineConfig:
33-        return replace(self, show_git_branch=v)
tests/test_slash_clear.py
20-
21-class TestClearSpecRegistration(unittest.TestCase):
22-    def test_clear_spec_present_with_local_execution_kind(self) -> None:
23:        clear_specs = [spec for spec in SLASH_COMMAND_SPECS if spec.name == "clear"]
24-        self.assertEqual(len(clear_specs), 1, "exactly one /clear spec must be registered")
25-        spec = clear_specs[0]
26-        self.assertEqual(spec.execution_kind, "local")
comate_cli/terminal_agent/statusline/picker_state.py
21-
22-# 固定顺序：与 statusline 渲染顺序一致
23-_FIELDS: tuple[tuple[str, str], ...] = (
24:    ("show_model_name", "Model Name"),
25-    ("show_llm_level", "LLM Level"),
26-    ("show_turn_number", "Turn Number"),
27-    ("show_project_cwd", "Project CWD"),
tests/test_slash_argument_hint.py
20-                argument_hint="[issue-number] [priority]",
21-            )
22-        }
23:        suggest = SlashArgumentHintAutoSuggest(resolve_spec=lambda name: spec_map.get(name))
24-
25-        suggestion = suggest.get_suggestion(None, Document(text="/fix-issue ", cursor_position=11))
26-
37-                argument_hint="[pattern]",
38-            )
39-        }
40:        suggest = SlashArgumentHintAutoSuggest(resolve_spec=lambda name: spec_map.get(name))
41-
42-        suggestion = suggest.get_suggestion(None, Document(text="/test", cursor_position=5))
43-        self.assertIsNone(suggestion)
43-        self.assertIsNone(suggestion)
44-
45-    def test_no_suggestion_for_unknown_command(self) -> None:
46:        suggest = SlashArgumentHintAutoSuggest(resolve_spec=lambda _name: None)
47-        suggestion = suggest.get_suggestion(None, Document(text="/unknown ", cursor_position=9))
48-        self.assertIsNone(suggestion)
49-
comate_cli/terminal_agent/tool_result_formatters.py
122-    metadata = rec.metadata if isinstance(rec.metadata, Mapping) else {}
123-    metrics: list[str] = []
124-
125:    model_name = str(metadata.get("model_name") or "").strip()
126-    if model_name:
127-        metrics.append(model_name)
128-
123-    metrics: list[str] = []
124-
125-    model_name = str(metadata.get("model_name") or "").strip()
126:    if model_name:
127-        metrics.append(model_name)
128-
129-    duration = _format_metric_duration_ms(metadata.get("duration_ms"))
124-
125-    model_name = str(metadata.get("model_name") or "").strip()
126-    if model_name:
127:        metrics.append(model_name)
128-
129-    duration = _format_metric_duration_ms(metadata.get("duration_ms"))
130-    if duration:
comate_cli/terminal_agent/event_renderer.py
260-    subagent_description: str = ""
261-    nested_tools: list[tuple[str, _SubagentTool]] = field(default_factory=list)
262-    show_init: bool = False
263:    subagent_model_name: str = ""
264-    awaiting_approval: bool = False
265-    approval_wait_started_at_monotonic: float | None = None
266-    approval_wait_seconds: float = 0.0
1351-                tool_count = len(state.nested_tools)
1352-                tool_count_suffix = f" {BULLET_OPERATOR} +{tool_count} tool uses" if tool_count > 0 else ""
1353-                rest = f" {BULLET_OPERATOR} {elapsed}{tool_count_suffix}{tokens_suffix}"
1354:                if state.subagent_model_name:
1355-                    # Return styled fragments: model_name in dim
1356-                    frags: list[tuple[str, str]] = [
1357-                        ("", title),
1352-                tool_count_suffix = f" {BULLET_OPERATOR} +{tool_count} tool uses" if tool_count > 0 else ""
1353-                rest = f" {BULLET_OPERATOR} {elapsed}{tool_count_suffix}{tokens_suffix}"
1354-                if state.subagent_model_name:
1355:                    # Return styled fragments: model_name in dim
1356-                    frags: list[tuple[str, str]] = [
1357-                        ("", title),
1358-                        ("class:dim", f" {BULLET_OPERATOR} {state.subagent_model_name}"),
1355-                    # Return styled fragments: model_name in dim
1356-                    frags: list[tuple[str, str]] = [
1357-                        ("", title),
1358:                        ("class:dim", f" {BULLET_OPERATOR} {state.subagent_model_name}"),
1359-                        ("", rest),
1360-                    ]
1361-                    entries.append((0, frags))
1428-        signature: str,
1429-        is_error: bool = False,
1430-        diff_lines: list[str] | None = None,
1431:        model_name: str = "",
1432-        subtitle: str | None = None,
1433-        file_path: str | None = None,
1434-    ) -> None:
1438-            signature: 工具签名，例如 "Read(path=xxx)"
1439-            is_error: 是否为错误结果
1440-            diff_lines: optional diff lines for Edit/Write
1441:            model_name: model name for Agent tools (rendered dim)
1442-            subtitle: optional subtitle rendered as `⎿ ...`
1443-            file_path: optional source path used for syntax highlighting diff body
1444-        """
1446-        sub_sty: Literal["error", "warning", "dim"] = "dim"
1447-        if not is_error and diff_lines and len(diff_lines) > 0:
1448-            text_obj = Text(signature)
1449:            if model_name:
1450-                text_obj.append(f" {BULLET_OPERATOR} {model_name}", style=theme.TEXT_FAINT)
1451-            text_obj.append("\n")
1452-            text_obj.append(render_diff_text(diff_lines, file_path=file_path))
1447-        if not is_error and diff_lines and len(diff_lines) > 0:
1448-            text_obj = Text(signature)
1449-            if model_name:
1450:                text_obj.append(f" {BULLET_OPERATOR} {model_name}", style=theme.TEXT_FAINT)
1451-            text_obj.append("\n")
1452-            text_obj.append(render_diff_text(diff_lines, file_path=file_path))
1453-            self._append_history_entry(
1454-                HistoryEntry(entry_type="tool_result", text=text_obj, severity="info", subtitle=subtitle)
1455-            )
1456-            return
1457:        if model_name:
1458-            text_obj = Text(signature)
1459-            text_obj.append(f" {BULLET_OPERATOR} {model_name}", style=theme.TEXT_FAINT)
1460-            self._append_history_entry(
1456-            return
1457-        if model_name:
1458-            text_obj = Text(signature)
1459:            text_obj.append(f" {BULLET_OPERATOR} {model_name}", style=theme.TEXT_FAINT)
1460-            self._append_history_entry(
1461-                HistoryEntry(entry_type="tool_result", text=text_obj, severity=sev, subtitle=subtitle, subtitle_style=sub_sty)
1462-            )
1533-        if state.is_task:
1534-            subagent_name = state.subagent_name or "Agent"
1535-            description = state.subagent_description or state.title
1536:            model_name = ""
1537-            if metadata and isinstance(metadata, dict):
1538-                model_name = metadata.get("model_name", "")
1539-            if not model_name:
1535-            description = state.subagent_description or state.title
1536-            model_name = ""
1537-            if metadata and isinstance(metadata, dict):
1538:                model_name = metadata.get("model_name", "")
1539-            if not model_name:
1540-                model_name = state.subagent_model_name
1541-
1536-            model_name = ""
1537-            if metadata and isinstance(metadata, dict):
1538-                model_name = metadata.get("model_name", "")
1539:            if not model_name:
1540-                model_name = state.subagent_model_name
1541-
1542-            tool_count = len(state.nested_tools)
1537-            if metadata and isinstance(metadata, dict):
1538-                model_name = metadata.get("model_name", "")
1539-            if not model_name:
1540:                model_name = state.subagent_model_name
1541-
1542-            tool_count = len(state.nested_tools)
1543-            display_name = state.display_tool_name or state.tool_name
1542-            tool_count = len(state.nested_tools)
1543-            display_name = state.display_tool_name or state.tool_name
1544-            metadata = dict(metadata or {})
1545:            if model_name:
1546-                metadata["model_name"] = model_name
1547-            elapsed_ms = max(
1548-                (
1543-            display_name = state.display_tool_name or state.tool_name
1544-            metadata = dict(metadata or {})
1545-            if model_name:
1546:                metadata["model_name"] = model_name
1547-            elapsed_ms = max(
1548-                (
1549-                    time.monotonic()
1944-                status=status,
1945-                elapsed_ms=elapsed_ms,
1946-                tokens=tokens,
1947:                model_name=model_name,
1948-            ):
1949-                state = self._running_tools.get(tool_call_id)
1950-                if state is not None:
1957-                    state.subagent_name = str(subagent_name or "").strip()
1958-                    state.subagent_status = str(status or "").strip()
1959-                    state.subagent_description = str(description or "").strip()
1960:                    if model_name:
1961-                        state.subagent_model_name = str(model_name)
1962-                    # 首个有效子事件后移除 init 占位。
1963-                    progress_status = str(status or "").strip().lower()
1958-                    state.subagent_status = str(status or "").strip()
1959-                    state.subagent_description = str(description or "").strip()
1960-                    if model_name:
1961:                        state.subagent_model_name = str(model_name)
1962-                    # 首个有效子事件后移除 init 占位。
1963-                    progress_status = str(status or "").strip().lower()
1964-                    has_activity = bool(tokens and int(tokens) > 0) or bool(
tests/test_skills_slash_command.py
366-        self.assertIsNone(registry.resolve("hidden"))
367-        self.assertEqual(registry.resolve("help").source, "builtin")  # type: ignore[union-attr]
368-        self.assertEqual(app._skill_slash_command_names, {"demo"})
369:        self.assertIn("demo", {spec.name for spec in completer.updated_specs})
370-
371-    def test_sync_registers_builtin_skillify_with_argument_hint(self) -> None:
372-        registry = SlashCommandRegistry()
387-        self.assertIsNotNone(skillify.spec.argument_hint)
388-        self.assertIn("description", skillify.spec.argument_hint or "")
389-        self.assertIn("skillify", app._skill_slash_command_names)
390:        self.assertIn("skillify", {spec.name for spec in completer.updated_specs})
391-
392-    def test_sync_registers_builtin_loop_with_argument_hint(self) -> None:
393-        registry = SlashCommandRegistry()
408-        self.assertEqual(loop.allow_when_busy, False)
409-        self.assertEqual(loop.spec.argument_hint, "[--durable] [interval] <prompt>")
410-        self.assertIn("loop", app._skill_slash_command_names)
411:        self.assertIn("loop", {spec.name for spec in completer.updated_specs})
412-
413-    def test_skill_frontmatter_argument_hint_reaches_slash_auto_suggest(self) -> None:
414-        registry = SlashCommandRegistry()
472-        loop = registry.resolve("loop")
473-        self.assertIsNotNone(loop)
474-        self.assertEqual(app._skill_slash_command_names, {"loop"})
475:        self.assertIn("loop", {spec.name for spec in completer.updated_specs})
476-
477-    def test_custom_slash_loop_takes_precedence_over_builtin_skill(self) -> None:
478-        registry = SlashCommandRegistry()
498-        assert loop is not None
499-        self.assertEqual(loop.source, "builtin")
500-        self.assertEqual(app._skill_slash_command_names, {"skillify"})
501:        self.assertIn("skillify", {spec.name for spec in completer.updated_specs})
502-
503-
504-if __name__ == "__main__":
comate_cli/terminal_agent/status_bar.py
56-
57-    def __init__(self, session: ChatSession):
58-        self._session = session
59:        self._model_name: str = self._resolve_model_name(session)
60-        self._mode: str = "act"
61-        self._git_branch: str = "N/A"
62-        self._context_used_pct: float = 0.0
89-        """为 /statusline UI 提供实时预览数据。"""
90-        parts: list[tuple[str, str]] = []
91-
92:        parts.append(("show_model_name", self._model_name))
93-        parts.append(("show_git_branch", f"~{self._git_branch}"))
94-        parts.append(("show_context_left", self.context_left_text()))
95-        parts.append(("show_git_diff", self._git_diff_preview()))
120-
121-    # ---- data resolvers -------------------------------------------------
122-    @staticmethod
123:    def _resolve_model_name(session: ChatSession) -> str:
124-        agent = getattr(session, "_agent", None)
125-        llm = getattr(agent, "llm", None)
126-        model = getattr(llm, "model", "")
144-        token_cost = getattr(agent, "_token_cost", None)
145-        return bool(getattr(token_cost, "include_cost", False))
146-
147:    def set_model_name(self, model_name: str) -> None:
148-        normalized = str(model_name).strip()
149-        self._model_name = normalized or "unknown-model"
150-
145-        return bool(getattr(token_cost, "include_cost", False))
146-
147-    def set_model_name(self, model_name: str) -> None:
148:        normalized = str(model_name).strip()
149-        self._model_name = normalized or "unknown-model"
150-
151-    def set_session(self, session: ChatSession) -> None:
146-
147-    def set_model_name(self, model_name: str) -> None:
148-        normalized = str(model_name).strip()
149:        self._model_name = normalized or "unknown-model"
150-
151-    def set_session(self, session: ChatSession) -> None:
152-        self._session = session
150-
151-    def set_session(self, session: ChatSession) -> None:
152-        self._session = session
153:        self._model_name = self._resolve_model_name(session)
154-        try:
155-            self._mode = self._resolve_session_mode(session)
156-        except Exception:
436-        mode_text = f"[{self._mode}]"
437-        branch_text = f"~{self._git_branch}"
438-        context_text = self.context_left_text()
439:        full_text = f"{mode_text} {self._model_name} | {branch_text} / {context_text}"
440-        budget = max(len(context_text), width)
441-        if len(full_text) <= budget:
442-            return full_text
441-        if len(full_text) <= budget:
442-            return full_text
443-
444:        prefix = f"{mode_text} {self._model_name} | {branch_text} / "
445-        prefix_budget = max(0, budget - len(context_text))
446-        trimmed_prefix = self._truncate_text(prefix, prefix_budget)
447-        return f"{trimmed_prefix}{context_text}"
491-                    f"Waiting for approval: {self._waiting_approval_tool}",
492-                )
493-            )
494:        if cfg.show_model_name:
495-            items.append((f"fg:{self._COLOR_MODEL}", self._model_name))
496-        goal_text = self._goal_status_text()
497-        if goal_text:
492-                )
493-            )
494-        if cfg.show_model_name:
495:            items.append((f"fg:{self._COLOR_MODEL}", self._model_name))
496-        goal_text = self._goal_status_text()
497-        if goal_text:
498-            goal_item = (f"fg:{self._COLOR_GOAL}", goal_text)
comate_cli/terminal_agent/tui.py
1034-        }
1035-
1036-        for spec in SLASH_COMMAND_SPECS:
1037:            handler = handlers.get(spec.name)
1038-            if handler is None:
1039-                logger.warning(f"missing slash command handler: {spec.name}")
1040-                continue
1036-        for spec in SLASH_COMMAND_SPECS:
1037-            handler = handlers.get(spec.name)
1038-            if handler is None:
1039:                logger.warning(f"missing slash command handler: {spec.name}")
1040-                continue
1041-            if spec.name in allow_when_busy and spec.execution_kind != "local":
1042-                raise ValueError(
1038-            if handler is None:
1039-                logger.warning(f"missing slash command handler: {spec.name}")
1040-                continue
1041:            if spec.name in allow_when_busy and spec.execution_kind != "local":
1042-                raise ValueError(
1043-                    f"slash command /{spec.name} cannot be allow_when_busy when "
1044-                    f"execution_kind={spec.execution_kind}"
1040-                continue
1041-            if spec.name in allow_when_busy and spec.execution_kind != "local":
1042-                raise ValueError(
1043:                    f"slash command /{spec.name} cannot be allow_when_busy when "
1044-                    f"execution_kind={spec.execution_kind}"
1045-                )
1046-            self._slash_registry.register(
1046-            self._slash_registry.register(
1047-                spec=spec,
1048-                handler=handler,
1049:                allow_when_busy=spec.name in allow_when_busy,
1050-                source="builtin",
1051-                argument_hint=spec.argument_hint,
1052-            )
1055-    def _load_custom_slash_commands(self) -> None:
1056-        builtin_names: set[str] = set()
1057-        for spec in SLASH_COMMAND_SPECS:
1058:            builtin_names.add(spec.name)
1059-            builtin_names.update(spec.aliases)
1060-
1061-        # default scope（随包 md 资源）先加载，fail-fast；不进 builtin_names，
1148-        if slash_completer is not None:
1149-            slash_completer.update_commands(self._slash_registry.command_specs())
1150-
1151:    def _resolve_slash_spec(self, name: str) -> SlashCommandSpec | None:
1152-        entry = self._slash_registry.resolve(name)
1153-        if entry is None:
1154-            return None
tests/test_event_renderer.py
647-                status="running",
648-                elapsed_ms=40_800,
649-                tokens=20_900,
650:                model_name="glm-5.1",
651-            )
652-        )
653-        renderer.handle_event(
comate_cli/terminal_agent/tui_parts/slash_command_registry.py
21-class SlashCommandRegistry:
22-    def __init__(self) -> None:
23-        self._entries_by_name: dict[str, RegisteredSlashCommand] = {}
24:        self._entries_by_spec_name: dict[str, RegisteredSlashCommand] = {}
25-
26-    def register(
27-        self,
39-            source=source,
40-            argument_hint=argument_hint,
41-        )
42:        names = (spec.name, *spec.aliases)
43-        for name in names:
44-            if name in self._entries_by_name:
45-                raise ValueError(f"Duplicate slash command registration: {name}")
45-                raise ValueError(f"Duplicate slash command registration: {name}")
46-        for name in names:
47-            self._entries_by_name[name] = entry
48:        self._entries_by_spec_name[spec.name] = entry
49-
50-    def unregister(self, name: str) -> None:
51-        """Remove a registered command by name or alias.
51-        """Remove a registered command by name or alias.
52-
53-        Removes the entry from both _entries_by_name (for all aliases) and
54:        _entries_by_spec_name. Silent no-op if name doesn't exist.
55-        """
56-        entry = self._entries_by_name.get(name)
57-        if entry is None:
60-        names_to_remove = [n for n, e in self._entries_by_name.items() if e is entry]
61-        for n in names_to_remove:
62-            del self._entries_by_name[n]
63:        self._entries_by_spec_name.pop(entry.spec.name, None)
64-
65-    def resolve(self, name: str) -> RegisteredSlashCommand | None:
66-        return self._entries_by_name.get(name)
66-        return self._entries_by_name.get(name)
67-
68-    def command_specs(self) -> tuple[SlashCommandSpec, ...]:
69:        return tuple(entry.spec for entry in self._entries_by_spec_name.values())
comate_cli/terminal_agent/tui_parts/render_panels.py
468-
469-        spec = entry.spec
470-        return Completion(
471:            text=f"/{spec.name}",
472-            start_position=-len(text_before_cursor),
473-            display=spec.slash_name(),
474-            display_meta=spec.description,
470-        return Completion(
471-            text=f"/{spec.name}",
472-            start_position=-len(text_before_cursor),
473:            display=spec.slash_name(),
474-            display_meta=spec.description,
475-        )
476-
comate_cli/terminal_agent/tui_parts/history_sync.py
274-                        subtitle_metadata,
275-                    )
276-
277:                # Extract model_name for Agent tools
278-                model_name = ""
279-                if tool_name.lower() == "agent":
280-                    exec_meta = item_metadata.get("tool_execution_meta")
275-                    )
276-
277-                # Extract model_name for Agent tools
278:                model_name = ""
279-                if tool_name.lower() == "agent":
280-                    exec_meta = item_metadata.get("tool_execution_meta")
281-                    if isinstance(exec_meta, dict):
279-                if tool_name.lower() == "agent":
280-                    exec_meta = item_metadata.get("tool_execution_meta")
281-                    if isinstance(exec_meta, dict):
282:                        model_name = exec_meta.get("model_name", "")
283-
284-                # 直接追加静态 HistoryEntry（无计时器）
285-                self._renderer.append_static_tool_result(
286-                    signature,
287-                    is_error,
288-                    diff_lines=diff_lines,
289:                    model_name=model_name,
290-                    subtitle=subtitle,
291-                    file_path=diff_file_path,
292-                )
comate_cli/terminal_agent/tui_parts/commands.py
151-            is_busy = self._busy
152-            if is_busy and not entry.allow_when_busy:
153-                self._append_slash_command_result(
154:                    f"A task is currently running. /{entry.spec.name} is not available right now.",
155-                    severity="error",
156-                )
157-                self._refresh_layers()
183-            alias_text = ""
184-            if spec.aliases:
185-                alias_text = f" ({', '.join(f'/{alias}' for alias in spec.aliases)})"
186:            lines.append(f"/{spec.name}{alias_text} - {spec.description}")
187-        self._append_slash_command_result("\n".join(lines))
188-
189-    def _slash_session(self, _args: str) -> None:
1751-            llm_level = level  # type: ignore[assignment]
1752-            event = self._session.set_level(llm_level)
1753-
1754:            # Get model names for display
1755-            prev_model = event.previous_model or "unknown"
1756-            new_model = event.new_model or "unknown"
1757-
1761-            )
1762-            logger.info(f"Model level switched: {event}")
1763-
1764:            # Update status bar model name using the new model name from event.
1765-            self._status_bar.set_model_name(new_model)
1766-            self._invalidate()
1767-        except Exception as e:
1762-            logger.info(f"Model level switched: {event}")
1763-
1764-            # Update status bar model name using the new model name from event.
1765:            self._status_bar.set_model_name(new_model)
1766-            self._invalidate()
1767-        except Exception as e:
1768-            self._append_slash_command_result(
1771-            )
1772-
1773-    def _update_status_bar_model(self) -> None:
1774:        """Update status bar with current model name from session."""
1775-        try:
1776-            agent = getattr(self._session, "_agent", None)
1777-            llm = getattr(agent, "llm", None)
1777-            llm = getattr(agent, "llm", None)
1778-            model = getattr(llm, "model", "")
1779-            if model:
1780:                self._status_bar.set_model_name(str(model))
1781-                self._invalidate()
1782-        except Exception:
1783-            logger.exception("Failed to update status bar model name")
1780-                self._status_bar.set_model_name(str(model))
1781-                self._invalidate()
1782-        except Exception:
1783:            logger.exception("Failed to update status bar model name")
1784-
1785-    def _enter_selection_mode(self) -> None:
1786-        """Enter model selection menu mode."""
comate_cli/terminal_agent/rpc/session_host.py
50-def _builtin_command_names() -> set[str]:
51-    names: set[str] = set()
52-    for spec in SLASH_COMMAND_SPECS:
53:        names.add(spec.name)
54-        names.update(spec.aliases)
55-    return names
56-
comate_cli/terminal_agent/config/store.py
179-    if parsed_bool is not None:
180-        snapshot = snapshot.with_display_thinking(parsed_bool)
181-
182:    model = _named_object(
183-        section,
184-        _CLI_CONFIG_MODEL_SECTION,
185-        "comate_cli.config.model",
184-        _CLI_CONFIG_MODEL_SECTION,
185-        "comate_cli.config.model",
186-    )
187:    raw_model = _named_string(model, "level", "comate_cli.config.model.level")
188-    if raw_model is not None:
189-        normalized = raw_model.strip().lower()
190-        if normalized in _VALID_MODEL_LEVELS:

[Showing results with pagination = limit: 250]