docs/cli-rpc-stdio-guide.md
17-| `comate resume <session_id> --rpc-stdio` | v3 的恢复由 `session.open.params.session_id` 显式声明，不走进程级 resume。 |
18-| `comate resume --rpc-stdio` | RPC stdio 不打开本地历史选择 UI。 |
19:| `comate --rpc-stdio --yolo` | 权限模式由每个 `session.open.params.permission_mode` 显式声明。 |
20-| `comate -p ... --rpc-stdio` | print mode 是另一条 one-shot 输出路径。 |
21-
49-
50-```json
51:{"jsonrpc":"2.0","id":"r-init","result":{"protocol_version":3,"capabilities":{"multi_session":true,"permission_requests":true,"plan_approval":true,"set_permission_mode":true,"partial_messages":true,"sessions_read":true,"session_status":true,"custom_commands":true},"default_cwd":"/workspace/project"}}
52-```
53-
65-| `permission_requests` | server 会通过 `permission.request` 请求逐工具审批。 |
66-| `plan_approval` | 支持 plan 模式、approve/reject plan。 |
67:| `set_permission_mode` | 支持运行时切换 session permission mode。 |
68-| `partial_messages` | `session.open.include_partial_messages=true` 时会发送 transient delta event。 |
69-| `sessions_read` | 支持历史会话读侧方法。 |
119-| `cwd` | string | 否 | session 工作目录，缺省为进程启动时的 project root。 |
120-| `session_id` | string | 否 | 恢复已有 session。缺省创建新 session。 |
121:| `permission_mode` | string | 否 | `default` / `acceptEdits` / `plan` / `bypassPermissions`，缺省 `default`。 |
122-| `include_partial_messages` | boolean | 否 | 是否发送 transient delta/progress event，缺省 `false`。 |
123-
125-
126-```json
127:{"session_handle":"s-1","session_id":"...","permission_mode":"default"}
128-```
129-
160-| `mode` | string | 是 | `default` / `acceptEdits` / `plan` / `bypassPermissions`。 |
161-
162:结果：`{"permission_mode":"...","previous":"..."}`。mode 变化时会发 `PermissionModeChanged` 事件。
163-
164-错误：`SESSION_NOT_FOUND`、`INVALID_PARAMS`。
173-| `clear_context` | boolean | 否 | 是否按 SDK plan approve 语义清理上下文。缺省 `false`。 |
174-
175:结果：`{"accepted":true,"permission_mode":"default"}`。
176-
177-错误：`SESSION_NOT_FOUND`、`INVALID_STATE`、`INVALID_PARAMS`。
181-参数：`session_handle`。
182-
183:结果：`{"rejected":true,"permission_mode":"..."}`。
184-
185-错误：`SESSION_NOT_FOUND`、`INVALID_STATE`、`INVALID_PARAMS`。
195-| `session_id` | SDK session id。 |
196-| `run_state` | `idle` / `running` / `waiting_for_input` / `waiting_for_plan_approval`。 |
197:| `permission_mode` | 当前权限模式。 |
198-| `model_level` | 当前模型档位。 |
199-| `usage` | SDK usage snapshot。 |
333-```json
334-{"jsonrpc":"2.0","id":"r-init","method":"rpc.initialize","params":{"capabilities":{"supports_permission_requests":true}}}
335:{"jsonrpc":"2.0","id":"r-open","method":"session.open","params":{"permission_mode":"default"}}
336-{"jsonrpc":"2.0","id":"r-query","method":"session.query","params":{"session_handle":"s-1","prompt":"用一句话解释 RPC"}}
337-```
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-
390-
391-```json
392:{"jsonrpc":"2.0","id":"r-open","method":"session.open","params":{"permission_mode":"plan"}}
393-{"jsonrpc":"2.0","id":"r-query","method":"session.query","params":{"session_handle":"s-1","prompt":"实现这个功能"}}
394-```
430-## 安全默认
431-
432:- `session.open` 缺省 `permission_mode` 是 `default`。
433-- `bypassPermissions` 必须由 client 在 `session.open` 或 `session.setPermissionMode` 显式传入。
434-- `--yolo` 不支持与 `--rpc-stdio` 混用。
432-- `session.open` 缺省 `permission_mode` 是 `default`。
433-- `bypassPermissions` 必须由 client 在 `session.open` 或 `session.setPermissionMode` 显式传入。
434:- `--yolo` 不支持与 `--rpc-stdio` 混用。
435-- `updatedPermissions` 只能表达 SDK 支持的权限更新目的地，例如 `localSettings` 或 `session`；具体复评估由 SDK 完成。
436-- RPC 不裁剪工具集；工具集与终端 comate 一致。
439-
440-- 每次 `session.open` 分配一个进程内 handle：`s-1`、`s-2`、`s-3`，单调递增，不复用。
441:- 每个 handle 对应一个独立 `ChatSession`、event pump、permission mode、approval bridge。
442-- `sequence` 是进程全局单调递增，跨 session 共享。
443-- 单个 session pump 崩溃时只移除该 session，并发 server-scope `SessionClosed`；其他 sessions 继续服务。
tests/test_rpc_session_host.py
31-        "cwd": _DEFAULT_CWD,
32-        "resume_session_id": None,
33:        "permission_mode": "default",
34-        "include_partial_messages": False,
35-    }
52-        self.sent: list[str] = []
53-        self.run_controller = _FakeRunController()
54:        self.permission_mode = "default"
55-        self.approval_callback: Any = None
56-        self.shutdown_called = False
77-        return f"m-{len(self.sent)}"
78-
79:    def get_permission_mode(self) -> str:
80-        return self.permission_mode
81-
78-
79-    def get_permission_mode(self) -> str:
80:        return self.permission_mode
81-
82-    def set_permission_mode(self, mode: str) -> str:
80-        return self.permission_mode
81-
82:    def set_permission_mode(self, mode: str) -> str:
83-        self.permission_mode = mode
84-        return mode
81-
82-    def set_permission_mode(self, mode: str) -> str:
83:        self.permission_mode = mode
84-        return mode
85-
93-        if self.pending_plan is None:
94-            raise ChatSessionError("no pending plan approval")
95:        self.permission_mode = "default"
96-        return "returned execution prompt"
97-
190-        host, created = _make_host(emitted)
191-        first = await host.open(_open_params())
192:        second = await host.open(_open_params(permission_mode="plan"))
193-        try:
194-            self.assertEqual(first.handle, "s-1")
196-            self.assertTrue(created[0].turn_start_enabled)
197-            self.assertIsInstance(created[0].approval_callback, RpcApprovalBridge)
198:            self.assertEqual(created[0].permission_mode, "default")
199-            self.assertEqual(created[1].permission_mode, "plan")
200-        finally:
197-            self.assertIsInstance(created[0].approval_callback, RpcApprovalBridge)
198-            self.assertEqual(created[0].permission_mode, "default")
199:            self.assertEqual(created[1].permission_mode, "plan")
200-        finally:
201-            await host.close_all()
315-            await host.close_all()
316-
317:    async def test_set_permission_mode_emits_mode_changed(self) -> None:
318-        host, fake, rpc_session, emitted = await self._open()
319-        try:
318-        host, fake, rpc_session, emitted = await self._open()
319-        try:
320:            result = await host.set_permission_mode(rpc_session.handle, "plan")
321-            self.assertEqual(result, {"permission_mode": "plan", "previous": "default"})
322-            self.assertIn("PermissionModeChanged", emitted.types())
319-        try:
320-            result = await host.set_permission_mode(rpc_session.handle, "plan")
321:            self.assertEqual(result, {"permission_mode": "plan", "previous": "default"})
322-            self.assertIn("PermissionModeChanged", emitted.types())
323-        finally:
327-        host, fake, rpc_session, emitted = await self._open()
328-        try:
329:            fake.permission_mode = "plan"
330-            fake.pending_plan = {
331-                "plan_path": "/tmp/plan.md",
336-            result = await host.approve_plan(rpc_session.handle, clear_context=False)
337-            self.assertEqual(result["accepted"], True)
338:            self.assertEqual(result["permission_mode"], "default")
339-            self.assertEqual(fake.sent, ["run the approved plan"])
340-            self.assertIn("PermissionModeChanged", emitted.types())
355-            status = await host.status(rpc_session.handle)
356-            self.assertEqual(status["session_id"], "sid-1")
357:            self.assertEqual(status["permission_mode"], "default")
358-            self.assertEqual(status["run_state"], "idle")
359-            self.assertIn("usage", status)
comate_cli/main.py
219-        "                     (repeatable). Applies to subagents too."
220-        "\n"
221:        "  --yolo             Start in bypassPermissions (YOLO) mode"
222-    )
223-
230-    positionals: list[str] = []
231-    add_dirs: list[str] = []
232:    yolo = False
233-
234-    index = 0
239-            index += 1
240-            continue
241:        if arg == "--yolo":
242-            yolo = True
243-            index += 1
240-            continue
241-        if arg == "--yolo":
242:            yolo = True
243-            index += 1
244-            continue
285-        index += 1
286-
287:    if rpc_stdio and yolo:
288-        raise _ArgumentError(
289-            "--yolo is not supported with --rpc-stdio; "
287-    if rpc_stdio and yolo:
288-        raise _ArgumentError(
289:            "--yolo is not supported with --rpc-stdio; "
290-            "set permission_mode per session via session.open"
291-        )
288-        raise _ArgumentError(
289-            "--yolo is not supported with --rpc-stdio; "
290:            "set permission_mode per session via session.open"
291-        )
292-
299-            raise _ArgumentError("-p/--print and --rpc-stdio are mutually exclusive")
300-        print_prompt = " ".join(positionals) if positionals else ""
301:        return rpc_stdio, None, False, print_prompt, output_format, tuple(add_dirs), yolo
302-
303-    # Non -p mode: original logic
303-    # Non -p mode: original logic
304-    if not positionals:
305:        return rpc_stdio, None, False, None, output_format, tuple(add_dirs), yolo
306-
307-    command = positionals[0]
316-
317-    if len(positionals) == 1:
318:        return rpc_stdio, None, True, None, output_format, tuple(add_dirs), yolo
319-
320-    if len(positionals) == 2:
319-
320-    if len(positionals) == 2:
321:        return rpc_stdio, positionals[1], False, None, output_format, tuple(add_dirs), yolo
322-
323-    raise _ArgumentError("resume accepts at most one <session_id>")
444-            print_output_format,
445-            add_dirs_raw,
446:            yolo,
447-        ) = _parse_args(run_argv)
448-        add_dirs = _validate_add_dirs(add_dirs_raw)
470-                print_output_format=print_output_format,
471-                add_dirs=add_dirs,
472:                yolo=yolo,
473-                process_start_perf=_PROCESS_START_PERF,
474-                app_import_start_perf=_app_import_start_perf,
tests/test_rpc_serde.py
17-from comate_cli.terminal_agent.rpc.serde import (
18-    SCHEMA_VERSION,
19:    build_permission_mode_changed_event,
20-    build_session_closed_event,
21-    is_transient_event,
108-        self.assertNotIn("request_id", envelope)
109-
110:    def test_permission_mode_changed(self) -> None:
111-        data = build_permission_mode_changed_event(
112-            permission_mode="plan", previous="default"
109-
110-    def test_permission_mode_changed(self) -> None:
111:        data = build_permission_mode_changed_event(
112-            permission_mode="plan", previous="default"
113-        )
110-    def test_permission_mode_changed(self) -> None:
111-        data = build_permission_mode_changed_event(
112:            permission_mode="plan", previous="default"
113-        )
114-        self.assertEqual(data["type"], "PermissionModeChanged")
113-        )
114-        self.assertEqual(data["type"], "PermissionModeChanged")
115:        self.assertEqual(data["payload"], {"permission_mode": "plan", "previous": "default"})
116-
117-    def test_session_closed(self) -> None:
tests/test_rpc_stdio_transport.py
59-        self.run_controller = _FakeRunController()
60-        self.approval_callback: Any = None
61:        self.permission_mode = "default"
62-        self.shutdown_calls = 0
63-
75-        return "m-1"
76-
77:    def get_permission_mode(self) -> str:
78-        return self.permission_mode
79-
76-
77-    def get_permission_mode(self) -> str:
78:        return self.permission_mode
79-
80-    def set_permission_mode(self, mode: str) -> str:
78-        return self.permission_mode
79-
80:    def set_permission_mode(self, mode: str) -> str:
81-        self.permission_mode = mode
82-        return mode
79-
80-    def set_permission_mode(self, mode: str) -> str:
81:        self.permission_mode = mode
82-        return mode
83-
186-                tool_call_id="tc-1",
187-                cwd=str(_DEFAULT_CWD),
188:                permission_mode="default",
189-                reason="ask",
190-                permission_suggestions=[],
236-                tool_call_id="tc-1",
237-                cwd=str(_DEFAULT_CWD),
238:                permission_mode="default",
239-                reason="ask",
240-                permission_suggestions=[],
267-                tool_call_id="tc-1",
268-                cwd=str(_DEFAULT_CWD),
269:                permission_mode="default",
270-                reason="ask",
271-                permission_suggestions=[],
tests/test_rpc_approval.py
18-    "tool_call_id": "tc-1",
19-    "cwd": "/tmp/project",
20:    "permission_mode": "default",
21-    "reason": "policy ask",
22-    "permission_suggestions": [],
tests/test_app_startup_latency.py
441-        build_agent.assert_not_called()
442-
443:    async def test_rpc_stdio_yolo_returns_before_building_agent(self) -> None:
444-        with (
445-            patch.object(app_module, "_resolve_cli_project_root", return_value=Path("/tmp/project")),
451-            patch.object(app_module, "_build_agent") as build_agent,
452-        ):
453:            await app_module.run(rpc_stdio=True, yolo=True)
454-
455-        build_agent.assert_not_called()
574-        self.assertTrue(created_tuis[0].run_called.is_set())
575-
576:    async def test_run_threads_approval_bridge_and_yolo_to_tui(self) -> None:
577-        session = _fake_session()
578-        build_kwargs: dict[str, object] = {}
611-            patch.object(app_module, "_format_exit_usage_line", return_value=None),
612-        ):
613:            await app_module.run(yolo=True)
614-
615-        approval_bridge = build_kwargs["approval_bridge"]
616-        self.assertTrue(callable(approval_bridge))
617-        self.assertIs(tui_kwargs["approval_bridge"], approval_bridge)
618:        self.assertTrue(build_kwargs["yolo"])
619-        self.assertTrue(tui_kwargs["yolo"])
620-
617-        self.assertIs(tui_kwargs["approval_bridge"], approval_bridge)
618-        self.assertTrue(build_kwargs["yolo"])
619:        self.assertTrue(tui_kwargs["yolo"])
620-
621-    async def test_background_update_hint_marks_seen_and_uses_scrollback(self) -> None:
tests/test_status_bar.py
57-        self._mode = mode
58-
59:    def get_permission_mode(self) -> str:
60-        return self._mode
61-
61-
62-    def get_mode(self) -> str:
63:        raise AssertionError("status bar should use get_permission_mode first")
64-
65-
144-
145-    @patch.object(StatusBar, "_resolve_git_branch", return_value="main")
146:    async def test_refresh_prefers_canonical_permission_mode(self, _mock_branch) -> None:
147-        status_bar = StatusBar(_PermissionModeSession("acceptEdits"))
148-
151-        self.assertIn("[acceptEdits]", status_bar.footer_status_text())
152-
153:    def test_set_session_prefers_canonical_permission_mode(self) -> None:
154-        status_bar = StatusBar(_FakeSession())
155-
165-        self.assertIn("[bypassPermissions]", status_bar.footer_status_text())
166-
167:    def test_set_mode_preserves_canonical_permission_mode(self) -> None:
168-        status_bar = StatusBar(_FakeSession())
169-
tests/test_main_args_yolo.py
4-
5-
6:def test_yolo_flag_parsed() -> None:
7-    parsed = _parse_args(["--yolo"])
8-    assert parsed[-1] is True
5-
6-def test_yolo_flag_parsed() -> None:
7:    parsed = _parse_args(["--yolo"])
8-    assert parsed[-1] is True
9-
9-
10-
11:def test_resume_with_yolo() -> None:
12-    parsed = _parse_args(["resume", "abc123", "--yolo"])
13-    assert parsed[1] == "abc123"
10-
11-def test_resume_with_yolo() -> None:
12:    parsed = _parse_args(["resume", "abc123", "--yolo"])
13-    assert parsed[1] == "abc123"
14-    assert parsed[-1] is True
15-
16-
17:def test_resume_without_yolo_does_not_infer_bypass_from_session_history() -> None:
18-    parsed = _parse_args(["resume", "abc123"])
19-    assert parsed[1] == "abc123"
21-
22-
23:def test_default_no_yolo() -> None:
24-    parsed = _parse_args([])
25-    assert parsed[-1] is False
comate_cli/terminal_agent/app.py
421-    add_dirs: tuple[Path, ...] = (),
422-    system_role: str | None = None,
423:    yolo: bool = False,
424-    approval_bridge: Any | None = None,
425-) -> Agent:
454-        # Explicit projection is required: the SDK library default is
455-        # bypassPermissions for unattended embeddings, while CLI starts safe.
456:        permission_mode="bypassPermissions" if yolo else "default",
457-        tool_approval_callback=approval_bridge,
458-    )
702-    print_output_format: str = "text",
703-    add_dirs: tuple[Path, ...] = (),
704:    yolo: bool = False,
705-    process_start_perf: float | None = None,
706-    app_import_start_perf: float | None = None,
759-        return
760-
761:    if rpc_stdio and yolo:
762-        console.print(
763-            "[red]--yolo is not supported with --rpc-stdio; "
761-    if rpc_stdio and yolo:
762-        console.print(
763:            "[red]--yolo is not supported with --rpc-stdio; "
764-            "set permission_mode per session via session.open[/]"
765-        )
762-        console.print(
763-            "[red]--yolo is not supported with --rpc-stdio; "
764:            "set permission_mode per session via session.open[/]"
765-        )
766-        return
793-        add_dirs=add_dirs,
794-        system_role=profile.system_role,
795:        yolo=yolo,
796-        approval_bridge=approval_bridge,
797-    )
845-    status_bar = StatusBar(session)
846-    profiler.mark("status_bar.done")
847:    get_permission_mode = getattr(session, "get_permission_mode", None)
848-    status_bar.set_mode(
849-        get_permission_mode() if callable(get_permission_mode) else session.get_mode()
847-    get_permission_mode = getattr(session, "get_permission_mode", None)
848-    status_bar.set_mode(
849:        get_permission_mode() if callable(get_permission_mode) else session.get_mode()
850-    )
851-    try:
863-        renderer,
864-        approval_bridge=approval_bridge,
865:        yolo=yolo,
866-    )
867-    profiler.mark("tui.init.done")
comate_cli/terminal_agent/status_bar.py
135-    @classmethod
136-    def _resolve_session_mode(cls, session: ChatSession) -> str:
137:        get_permission_mode = getattr(session, "get_permission_mode", None)
138-        if callable(get_permission_mode):
139-            return cls._normalize_display_mode(get_permission_mode())
136-    def _resolve_session_mode(cls, session: ChatSession) -> str:
137-        get_permission_mode = getattr(session, "get_permission_mode", None)
138:        if callable(get_permission_mode):
139-            return cls._normalize_display_mode(get_permission_mode())
140-        return cls._normalize_display_mode(session.get_mode())
137-        get_permission_mode = getattr(session, "get_permission_mode", None)
138-        if callable(get_permission_mode):
139:            return cls._normalize_display_mode(get_permission_mode())
140-        return cls._normalize_display_mode(session.get_mode())
141-
tests/test_rpc_options.py
19-        self.assertIsNone(params.resume_session_id)
20-        # 双默认值守卫：缺省必须是 default，绝不能是 bypassPermissions
21:        self.assertEqual(params.permission_mode, "default")
22-        self.assertFalse(params.include_partial_messages)
23-
22-        self.assertFalse(params.include_partial_messages)
23-
24:    def test_all_four_permission_modes_accepted(self) -> None:
25-        for mode in PERMISSION_MODES:
26-            params = parse_session_open_params(
25-        for mode in PERMISSION_MODES:
26-            params = parse_session_open_params(
27:                {"permission_mode": mode}, default_cwd=_DEFAULT_CWD
28-            )
29-            self.assertEqual(params.permission_mode, mode)
27-                {"permission_mode": mode}, default_cwd=_DEFAULT_CWD
28-            )
29:            self.assertEqual(params.permission_mode, mode)
30-
31-    def test_legacy_act_mode_rejected(self) -> None:
31-    def test_legacy_act_mode_rejected(self) -> None:
32-        with self.assertRaises(RpcOptionsError):
33:            parse_session_open_params({"permission_mode": "act"}, default_cwd=_DEFAULT_CWD)
34-
35-    def test_string_bool_rejected(self) -> None:
tests/test_main_args.py
23-            main_module._parse_args(["resume", "sid-001", "--rpc-stdio"])
24-
25:    def test_parse_args_rejects_yolo_with_rpc_stdio(self) -> None:
26-        with self.assertRaises(main_module._ArgumentError):
27-            main_module._parse_args(["--rpc-stdio", "--yolo"])
25-    def test_parse_args_rejects_yolo_with_rpc_stdio(self) -> None:
26-        with self.assertRaises(main_module._ArgumentError):
27:            main_module._parse_args(["--rpc-stdio", "--yolo"])
28-
29-    def test_parse_args_resume_without_session_id_enters_selection_mode(self) -> None:
253-    # --- --add-dir parsing ---
254-    def test_parse_args_default_add_dirs_empty(self) -> None:
255:        *_rest, add_dirs, yolo = main_module._parse_args([])
256-        self.assertEqual(add_dirs, ())
257-        self.assertFalse(yolo)
255-        *_rest, add_dirs, yolo = main_module._parse_args([])
256-        self.assertEqual(add_dirs, ())
257:        self.assertFalse(yolo)
258-
259-    def test_parse_args_add_dir_single(self) -> None:
258-
259-    def test_parse_args_add_dir_single(self) -> None:
260:        *_rest, add_dirs, yolo = main_module._parse_args(["--add-dir", "/some/path"])
261-        self.assertEqual(add_dirs, ("/some/path",))
262-        self.assertFalse(yolo)
260-        *_rest, add_dirs, yolo = main_module._parse_args(["--add-dir", "/some/path"])
261-        self.assertEqual(add_dirs, ("/some/path",))
262:        self.assertFalse(yolo)
263-
264-    def test_parse_args_add_dir_equals_form(self) -> None:
263-
264-    def test_parse_args_add_dir_equals_form(self) -> None:
265:        *_rest, add_dirs, yolo = main_module._parse_args(["--add-dir=/some/path"])
266-        self.assertEqual(add_dirs, ("/some/path",))
267-        self.assertFalse(yolo)
265-        *_rest, add_dirs, yolo = main_module._parse_args(["--add-dir=/some/path"])
266-        self.assertEqual(add_dirs, ("/some/path",))
267:        self.assertFalse(yolo)
268-
269-    def test_parse_args_add_dir_repeatable(self) -> None:
268-
269-    def test_parse_args_add_dir_repeatable(self) -> None:
270:        *_rest, add_dirs, yolo = main_module._parse_args(
271-            ["--add-dir", "/a", "--add-dir=/b"]
272-        )
272-        )
273-        self.assertEqual(add_dirs, ("/a", "/b"))
274:        self.assertFalse(yolo)
275-
276-    def test_parse_args_add_dir_requires_value(self) -> None:
284-    def test_parse_args_add_dir_value_not_treated_as_print_prompt(self) -> None:
285-        """`-p "hi" --add-dir /data` → prompt 'hi'，/data 不混入 prompt。"""
286:        _, _, _, print_prompt, _output_format, add_dirs, yolo = main_module._parse_args(
287-            ["-p", "hi", "--add-dir", "/data"]
288-        )
289-        self.assertEqual(print_prompt, "hi")
290-        self.assertEqual(add_dirs, ("/data",))
291:        self.assertFalse(yolo)
292-
293-    # --- _validate_add_dirs ---
345-            )
346-
347:    def test_main_passes_yolo_to_run(self) -> None:
348-        from unittest.mock import AsyncMock
349-
349-
350-        with patch("comate_cli.terminal_agent.app.run", new_callable=AsyncMock) as mock_run:
351:            main_module.main(["--yolo"])
352-        call_kwargs = mock_run.call_args.kwargs
353-        self.assertTrue(call_kwargs["yolo"])
351-            main_module.main(["--yolo"])
352-        call_kwargs = mock_run.call_args.kwargs
353:        self.assertTrue(call_kwargs["yolo"])
354-
355-    def test_app_run_accepts_add_dirs_kwarg(self) -> None:
360-        self.assertIn("add_dirs", inspect.signature(app.run).parameters)
361-
362:    def test_app_run_accepts_yolo_kwarg(self) -> None:
363-        import inspect
364-
365-        from comate_cli.terminal_agent import app
366-
367:        self.assertIn("yolo", inspect.signature(app.run).parameters)
368-
369-    def test_build_agent_passes_add_dirs_to_agent_config(self) -> None:
405-        self.assertEqual(captured["add_dirs"], (resolved,))
406-
407:    def test_build_agent_projects_cli_permission_mode_and_callback(self) -> None:
408-        from pathlib import Path
409-
439-        ):
440-            app._build_agent(project_root=Path("/tmp/project"))
441:            self.assertEqual(captured["permission_mode"], "default")
442-            self.assertIsNone(captured["tool_approval_callback"])
443-
445-            app._build_agent(
446-                project_root=Path("/tmp/project"),
447:                yolo=True,
448-                approval_bridge=approval_bridge,
449-            )
comate_cli/terminal_agent/tui.py
227-        *,
228-        approval_bridge: Any | None = None,
229:        yolo: bool = False,
230-    ) -> None:
231-        self._session = session
231-        self._session = session
232-        self._approval_bridge = approval_bridge
233:        self._yolo = yolo
234-        self._status_bar = status_bar
235-        try:
234-        self._status_bar = status_bar
235-        try:
236:            self._status_bar.set_mode(self._current_permission_mode())
237-        except Exception:
238-            pass
766-        self._sync_local_interactive_ui_barrier()
767-
768:    def _current_permission_mode(self) -> str:
769-        get_permission_mode = getattr(self._session, "get_permission_mode", None)
770-        if callable(get_permission_mode):
767-
768-    def _current_permission_mode(self) -> str:
769:        get_permission_mode = getattr(self._session, "get_permission_mode", None)
770-        if callable(get_permission_mode):
771-            return str(get_permission_mode())
768-    def _current_permission_mode(self) -> str:
769-        get_permission_mode = getattr(self._session, "get_permission_mode", None)
770:        if callable(get_permission_mode):
771-            return str(get_permission_mode())
772-        return str(self._session.get_mode())
769-        get_permission_mode = getattr(self._session, "get_permission_mode", None)
770-        if callable(get_permission_mode):
771:            return str(get_permission_mode())
772-        return str(self._session.get_mode())
773-
1963-        self._pending_plan_approval = None
1964-        try:
1965:            self._status_bar.set_mode(self._current_permission_mode())
1966-        except Exception:
1967-            pass
1978-        self._pending_plan_approval = None
1979-        try:
1980:            self._status_bar.set_mode(self._current_permission_mode())
1981-        except Exception:
1982-            pass
comate_cli/terminal_agent/rpc/options.py
13-
14-_ALLOWED_KEYS = frozenset(
15:    {"cwd", "session_id", "permission_mode", "include_partial_messages"}
16-)
17-
25-    cwd: Path
26-    resume_session_id: str | None
27:    permission_mode: str
28-    include_partial_messages: bool
29-
56-
57-    # 双默认值守卫（rules/permission-mode.md）：RPC 宿主层显式投影 default。
58:    mode_raw = raw.get("permission_mode")
59-    if mode_raw is None:
60-        permission_mode = "default"
58-    mode_raw = raw.get("permission_mode")
59-    if mode_raw is None:
60:        permission_mode = "default"
61-    elif isinstance(mode_raw, str) and mode_raw in PERMISSION_MODES:
62-        permission_mode = mode_raw
60-        permission_mode = "default"
61-    elif isinstance(mode_raw, str) and mode_raw in PERMISSION_MODES:
62:        permission_mode = mode_raw
63-    else:
64-        raise RpcOptionsError(
63-    else:
64-        raise RpcOptionsError(
65:            f"permission_mode must be one of {list(PERMISSION_MODES)}"
66-        )
67-
77-        cwd=cwd,
78-        resume_session_id=resume_session_id,
79:        permission_mode=permission_mode,
80-        include_partial_messages=include_partial_messages,
81-    )
comate_cli/terminal_agent/rpc/serde.py
65-
66-
67:def build_permission_mode_changed_event(
68-    *, permission_mode: str, previous: str
69-) -> dict[str, Any]:
66-
67-def build_permission_mode_changed_event(
68:    *, permission_mode: str, previous: str
69-) -> dict[str, Any]:
70-    return {
70-    return {
71-        "type": "PermissionModeChanged",
72:        "payload": {"permission_mode": permission_mode, "previous": previous},
73-    }
74-
comate_cli/terminal_agent/rpc/server.py
57-        "permission_requests": True,
58-        "plan_approval": True,
59:        "set_permission_mode": True,
60-        "partial_messages": True,
61-        "sessions_read": True,
320-            "session.query": self._handle_session_query,
321-            "session.interrupt": self._handle_session_interrupt,
322:            "session.setPermissionMode": self._handle_set_permission_mode,
323-            "session.approvePlan": self._handle_approve_plan,
324-            "session.rejectPlan": self._handle_reject_plan,
375-            "session_handle": rpc_session.handle,
376-            "session_id": rpc_session.chat_session.session_id,
377:            "permission_mode": rpc_session.chat_session.get_permission_mode(),
378-        }
379-
388-        return {"interrupted": self._host.interrupt(handle)}
389-
390:    async def _handle_set_permission_mode(self, params: dict[str, Any]) -> dict[str, Any]:
391-        handle = _require_str(params, "session_handle")
392-        mode = params.get("mode")
396-                message=f"mode must be one of {list(PERMISSION_MODES)}",
397-            )
398:        return await self._host.set_permission_mode(handle, mode)
399-
400-    async def _handle_approve_plan(self, params: dict[str, Any]) -> dict[str, Any]:
comate_cli/terminal_agent/tui_parts/commands.py
105-            return
106-        try:
107:            cycle_permission_mode = getattr(self._session, "cycle_permission_mode", None)
108-            if callable(cycle_permission_mode):
109-                mode = cycle_permission_mode()
106-        try:
107-            cycle_permission_mode = getattr(self._session, "cycle_permission_mode", None)
108:            if callable(cycle_permission_mode):
109-                mode = cycle_permission_mode()
110-            else:
107-            cycle_permission_mode = getattr(self._session, "cycle_permission_mode", None)
108-            if callable(cycle_permission_mode):
109:                mode = cycle_permission_mode()
110-            else:
111-                mode = self._session.cycle_mode()
comate_cli/terminal_agent/rpc/session_host.py
25-from comate_cli.terminal_agent.rpc.options import RpcSessionOpenParams
26-from comate_cli.terminal_agent.rpc.serde import (
27:    build_permission_mode_changed_event,
28-    build_session_closed_event,
29-    is_transient_event,
168-
169-        chat_session.enable_turn_start_events()
170:        chat_session.set_permission_mode(params.permission_mode)
171-        approval_bridge = RpcApprovalBridge(session_handle=handle, sender=self._sender)
172-        chat_session.set_tool_approval_callback(approval_bridge)
236-
237-    async def _emit_mode_changed(
238:        self, handle: str, *, permission_mode: str, previous: str
239-    ) -> None:
240-        if permission_mode == previous:
238-        self, handle: str, *, permission_mode: str, previous: str
239-    ) -> None:
240:        if permission_mode == previous:
241-            return
242-        await self._emit_event(
241-            return
242-        await self._emit_event(
243:            build_permission_mode_changed_event(
244-                permission_mode=permission_mode, previous=previous
245-            ),
242-        await self._emit_event(
243-            build_permission_mode_changed_event(
244:                permission_mode=permission_mode, previous=previous
245-            ),
246-            session_handle=handle,
248-        )
249-
250:    async def set_permission_mode(self, handle: str, mode: str) -> dict[str, Any]:
251-        rpc_session = self.require(handle)
252-        previous = rpc_session.chat_session.get_permission_mode()
250-    async def set_permission_mode(self, handle: str, mode: str) -> dict[str, Any]:
251-        rpc_session = self.require(handle)
252:        previous = rpc_session.chat_session.get_permission_mode()
253-        new_mode = rpc_session.chat_session.set_permission_mode(mode)
254-        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
251-        rpc_session = self.require(handle)
252-        previous = rpc_session.chat_session.get_permission_mode()
253:        new_mode = rpc_session.chat_session.set_permission_mode(mode)
254-        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
255-        return {"permission_mode": new_mode, "previous": previous}
252-        previous = rpc_session.chat_session.get_permission_mode()
253-        new_mode = rpc_session.chat_session.set_permission_mode(mode)
254:        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
255-        return {"permission_mode": new_mode, "previous": previous}
256-
253-        new_mode = rpc_session.chat_session.set_permission_mode(mode)
254-        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
255:        return {"permission_mode": new_mode, "previous": previous}
256-
257-    async def approve_plan(self, handle: str, *, clear_context: bool) -> dict[str, Any]:
259-        chat_session = rpc_session.chat_session
260-        pending = chat_session.get_pending_plan_approval() or {}
261:        previous = chat_session.get_permission_mode()
262-        prompt = chat_session.approve_plan(clear_context=clear_context)
263-        execution_prompt = str(pending.get("execution_prompt", "")).strip()
265-            prompt = execution_prompt
266-        await chat_session.send(prompt)
267:        new_mode = chat_session.get_permission_mode()
268-        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
269-        return {"accepted": True, "permission_mode": new_mode}
266-        await chat_session.send(prompt)
267-        new_mode = chat_session.get_permission_mode()
268:        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
269-        return {"accepted": True, "permission_mode": new_mode}
270-
267-        new_mode = chat_session.get_permission_mode()
268-        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
269:        return {"accepted": True, "permission_mode": new_mode}
270-
271-    async def reject_plan(self, handle: str) -> dict[str, Any]:
271-    async def reject_plan(self, handle: str) -> dict[str, Any]:
272-        rpc_session = self.require(handle)
273:        previous = rpc_session.chat_session.get_permission_mode()
274-        rpc_session.chat_session.reject_plan()
275-        new_mode = rpc_session.chat_session.get_permission_mode()
273-        previous = rpc_session.chat_session.get_permission_mode()
274-        rpc_session.chat_session.reject_plan()
275:        new_mode = rpc_session.chat_session.get_permission_mode()
276-        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
277-        return {"rejected": True, "permission_mode": new_mode}
274-        rpc_session.chat_session.reject_plan()
275-        new_mode = rpc_session.chat_session.get_permission_mode()
276:        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
277-        return {"rejected": True, "permission_mode": new_mode}
278-
275-        new_mode = rpc_session.chat_session.get_permission_mode()
276-        await self._emit_mode_changed(handle, permission_mode=new_mode, previous=previous)
277:        return {"rejected": True, "permission_mode": new_mode}
278-
279-    async def status(self, handle: str) -> dict[str, Any]:
285-            "session_id": chat_session.session_id,
286-            "run_state": rpc_session.run_state,
287:            "permission_mode": chat_session.get_permission_mode(),
288-            "model_level": getattr(getattr(chat_session, "_agent", None), "level", None),
289-            "usage": usage,
tests/test_rpc_server_methods.py
32-        self.sent: list[str] = []
33-        self.run_controller = _FakeRunController()
34:        self.permission_mode = "default"
35-        self.approval_callback: Any = None
36-        self.shutdown_called = False
51-        return "m-1"
52-
53:    def get_permission_mode(self) -> str:
54-        return self.permission_mode
55-
52-
53-    def get_permission_mode(self) -> str:
54:        return self.permission_mode
55-
56-    def set_permission_mode(self, mode: str) -> str:
54-        return self.permission_mode
55-
56:    def set_permission_mode(self, mode: str) -> str:
57-        self.permission_mode = mode
58-        return mode
55-
56-    def set_permission_mode(self, mode: str) -> str:
57:        self.permission_mode = mode
58-        return mode
59-
67-        if self.pending_plan is None:
68-            raise ChatSessionError("no pending plan approval")
69:        self.permission_mode = "default"
70-        return "execute"
71-
221-        await harness.initialize()
222-        first = await harness.call("session.open", {})
223:        second = await harness.call("session.open", {"permission_mode": "plan"})
224-        self.assertEqual(first["result"]["session_handle"], "s-1")
225-        self.assertEqual(first["result"]["permission_mode"], "default")
223-        second = await harness.call("session.open", {"permission_mode": "plan"})
224-        self.assertEqual(first["result"]["session_handle"], "s-1")
225:        self.assertEqual(first["result"]["permission_mode"], "default")
226-        self.assertEqual(second["result"]["session_handle"], "s-2")
227-        self.assertEqual(second["result"]["permission_mode"], "plan")
225-        self.assertEqual(first["result"]["permission_mode"], "default")
226-        self.assertEqual(second["result"]["session_handle"], "s-2")
227:        self.assertEqual(second["result"]["permission_mode"], "plan")
228-
229-        query = await harness.call(
262-        await harness.server.shutdown()
263-
264:    async def test_set_permission_mode_validates_enum(self) -> None:
265-        harness = _Harness()
266-        await harness.initialize()
269-            "session.setPermissionMode", {"session_handle": "s-1", "mode": "acceptEdits"}
270-        )
271:        self.assertEqual(ok["result"]["permission_mode"], "acceptEdits")
272-        bad = await harness.call(
273-            "session.setPermissionMode", {"session_handle": "s-1", "mode": "act"}
291-        await harness.call("session.open", {})
292-        status = await harness.call("session.status", {"session_handle": "s-1"})
293:        self.assertEqual(status["result"]["permission_mode"], "default")
294-        compact = await harness.call("session.compact", {"session_handle": "s-1"})
295-        self.assertEqual(compact["result"], {"compacted": True})
tests/test_permission_mode_cycle.py
6-class _Session:
7-    def __init__(self) -> None:
8:        self.cycle_permission_mode_calls = 0
9-
10-    def cycle_permission_mode(self) -> str:
8-        self.cycle_permission_mode_calls = 0
9-
10:    def cycle_permission_mode(self) -> str:
11-        self.cycle_permission_mode_calls += 1
12-        return "acceptEdits"
9-
10-    def cycle_permission_mode(self) -> str:
11:        self.cycle_permission_mode_calls += 1
12-        return "acceptEdits"
13-
49-    host._cycle_agent_mode()
50-
51:    assert host._session.cycle_permission_mode_calls == 1
52-    assert host._status_bar.modes == ["acceptEdits"]
53-    assert host._renderer.messages == []
tests/test_completion_status_panel.py
199-
200-class TestCompletionStatusPanel(unittest.TestCase):
201:    def test_footer_left_mode_hint_uses_four_permission_modes(self) -> None:
202-        cases = {
203-            "default": "Default mode",

[Showing results with pagination = limit: 200]