# Read: /home/hyc/projects/agent-sdk/packages/comate_cli/comate_cli/terminal_agent/tui.py

Lines 630-1329 of 1741 (TRUNCATED: line_limit)

   630	
   631	    def _should_show_queue_panel(self) -> bool:
   632	        return self._ui_mode == UIMode.NORMAL and bool(self._queued_human_messages())
   633	
   634	    def _build_slash_registry(self) -> None:
   635	        handlers: dict[str, Callable[[str], Any]] = {
   636	            "help": self._slash_help,
   637	            "model": self._slash_model,
   638	            "session": self._slash_session,
   639	            "usage": self._slash_usage,
   640	            "context": self._slash_context,
   641	            "skills": self._slash_skills,
   642	            "mcp": self._slash_mcp,
   643	            "compact": self._slash_compact,
   644	            "rewind": self._slash_rewind,
   645	            "exit": self._slash_exit,
   646	            "plugin": self._slash_plugin,
   647	            "reload-plugins": self._slash_reload_plugins,
   648	        }
   649	        allow_when_busy = {
   650	            "help",
   651	            "session",
   652	            "usage",
   653	            "context",
   654	            "skills",
   655	            "mcp",
   656	            "exit",
   657	            "plugin",
   658	            "reload-plugins",
   659	        }
   660	
   661	        for spec in SLASH_COMMAND_SPECS:
   662	            handler = handlers.get(spec.name)
   663	            if handler is None:
   664	                logger.warning(f"missing slash command handler: {spec.name}")
   665	                continue
   666	            if spec.name in allow_when_busy and spec.execution_kind != "local":
   667	                raise ValueError(
   668	                    f"slash command /{spec.name} cannot be allow_when_busy when "
   669	                    f"execution_kind={spec.execution_kind}"
   670	                )
   671	            self._slash_registry.register(
   672	                spec=spec,
   673	                handler=handler,
   674	                allow_when_busy=spec.name in allow_when_busy,
   675	                source="builtin",
   676	                argument_hint=spec.argument_hint,
   677	            )
   678	        self._load_custom_slash_commands()
   679	
   680	    def _load_custom_slash_commands(self) -> None:
   681	        builtin_names: set[str] = set()
   682	        for spec in SLASH_COMMAND_SPECS:
   683	            builtin_names.add(spec.name)
   684	            builtin_names.update(spec.aliases)
   685	
   686	        session_cwd = getattr(self._session, "_cwd", None)
   687	        project_root = Path(session_cwd).expanduser().resolve() if session_cwd else Path.cwd().expanduser().resolve()
   688	        result = discover_custom_slash_commands(
   689	            project_root=project_root,
   690	            builtin_names=builtin_names,
   691	        )
   692	        for warning in result.warnings:
   693	            self._renderer.append_system_message(warning, severity="warning")
   694	
   695	        self._custom_slash_commands = {command.name: command for command in result.commands}
   696	        for command in result.commands:
   697	            spec = SlashCommandSpec(
   698	                name=command.name,
   699	                description=command.description_with_scope(),
   700	                execution_kind="hybrid",
   701	                argument_hint=command.argument_hint,
   702	            )
   703	
   704	            async def _handler(args: str, *, command_name: str = command.name) -> None:
   705	                await self._slash_custom(command_name=command_name, args=args)
   706	
   707	            self._slash_registry.register(
   708	                spec=spec,
   709	                handler=_handler,
   710	                allow_when_busy=False,
   711	                source="custom",
   712	                argument_hint=command.argument_hint,
   713	            )
   714	
   715	    def _sync_skill_slash_commands(self) -> None:
   716	        """同步当前 runtime skills 到 slash command registry。"""
   717	        for name in list(getattr(self, "_skill_slash_command_names", set())):
   718	            self._slash_registry.unregister(name)
   719	        self._skill_slash_command_names = set()
   720	
   721	        agent = getattr(self._session, "_agent", None)
   722	        runtime_state = getattr(agent, "_runtime_state", None)
   723	        skills = list(getattr(runtime_state, "skills", []) or [])
   724	        seen_names: set[str] = set()
   725	
   726	        for skill in skills:
   727	            if not bool(getattr(skill, "user_invocable", True)):
   728	                continue
   729	
   730	            name = str(getattr(skill, "name", "")).strip()
   731	            if not name or name in seen_names:
   732	                continue
   733	            seen_names.add(name)
   734	
   735	            if self._slash_registry.resolve(name) is not None:
   736	                logger.warning(
   737	                    "Skill '%s' conflicts with an existing slash command, skipping",
   738	                    name,
   739	                )
   740	                continue
   741	
   742	            spec = SlashCommandSpec(
   743	                name=name,
   744	                description=str(getattr(skill, "description", "") or "").strip(),
   745	                execution_kind="hybrid",
   746	                argument_hint=getattr(skill, "argument_hint", None),
   747	            )
   748	
   749	            async def _handler(args: str, *, skill_name: str = name) -> None:
   750	                await self._slash_skill(skill_name=skill_name, args=args)
   751	
   752	            self._slash_registry.register(
   753	                spec=spec,
   754	                handler=_handler,
   755	                allow_when_busy=False,
   756	                source="skill",
   757	                argument_hint=spec.argument_hint,
   758	            )
   759	            self._skill_slash_command_names.add(name)
   760	
   761	        slash_completer = getattr(self, "_slash_completer", None)
   762	        if slash_completer is not None:
   763	            slash_completer.update_commands(self._slash_registry.command_specs())
   764	
   765	    def _resolve_slash_spec(self, name: str) -> SlashCommandSpec | None:
   766	        entry = self._slash_registry.resolve(name)
   767	        if entry is None:
   768	            return None
   769	        return entry.spec
   770	
   771	    def _create_input_history(self) -> FileHistory | InMemoryHistory:
   772	        """Create persistent FileHistory based on cwd, with fallback to InMemoryHistory."""
   773	        try:
   774	            session_cwd = getattr(self._session, "_cwd", None)
   775	            cwd = str(Path(session_cwd).expanduser().resolve()) if session_cwd else str(Path.cwd().resolve())
   776	            history_path = _get_input_history_path(cwd)
   777	        except Exception:
   778	            logger.debug("Failed to create FileHistory, falling back to InMemoryHistory", exc_info=True)
   779	            return InMemoryHistory()
   780	        try:
   781	            _truncate_file_history(history_path, _INPUT_HISTORY_MAX_ENTRIES)
   782	        except Exception:
   783	            logger.warning(
   784	                "Failed to normalize input history file %s; continuing with FileHistory",
   785	                history_path,
   786	                exc_info=True,
   787	            )
   788	        return FileHistory(str(history_path))
   789	
   790	    def _input_placeholder_hint(self) -> str | None:
   791	        if self._ui_mode != UIMode.NORMAL:
   792	            return None
   793	        queued_messages = self._queued_human_messages()
   794	        if not queued_messages:
   795	            return None
   796	        input_text = str(getattr(getattr(self, "_input_area", None), "text", ""))
   797	        if input_text.strip():
   798	            return None
   799	        if len(queued_messages) >= 2:
   800	            return self._queued_input_hint
   801	
   802	        preview = self._queued_message_preview(queued_messages[0])
   803	        max_chars = max(8, int(self._queued_preview_max_chars))
   804	        if len(preview) > max_chars:
   805	            preview = f"{preview[: max_chars - 3]}..."
   806	        return f"queued message: {preview}  |  {self._queued_input_hint}"
   807	
   808	    # 极客风运行完成词语，随机选一个拼成 history 记录
   809	    _RUN_ELAPSED_VERBS: tuple[str, ...] = (
   810	        "Executed",
   811	        "Compiled",
   812	        "Dispatched",
   813	        "Processed",
   814	        "Computed",
   815	        "Resolved",
   816	        "Terminated",
   817	        "Worked",
   818	        "Committed",
   819	        "Deployed",
   820	        "Brewed",
   821	        "Flushed",
   822	        "Finalized",
   823	        "Crunched",
   824	        "Propagated",
   825	        "Churned",
   826	        "Synchronized",
   827	        "Yielded",
   828	        "Emitted",
   829	    )
   830	
   831	    @staticmethod
   832	    def _format_run_elapsed(seconds: float) -> str:
   833	        """将秒数格式化为人类可读的时间字符串."""
   834	        if seconds < 10.0:
   835	            return f"{seconds:.1f}s"
   836	        if seconds < 60.0:
   837	            return f"{int(seconds)}s"
   838	        minutes = int(seconds) // 60
   839	        secs = int(seconds) % 60
   840	        return f"{minutes}m {secs}s"
   841	
   842	    def _append_run_elapsed_to_history(self, *, stop_reason: str | None = None) -> None:
   843	        """将本次 running 耗时以极客风格写入 history scrollback."""
   844	        if self._run_start_time is None:
   845	            return
   846	        elapsed = time.monotonic() - self._run_start_time
   847	        duration_str = self._format_run_elapsed(elapsed)
   848	        if str(stop_reason or "").strip().lower() == "interrupted":
   849	            self._renderer.append_elapsed_message(f"Interrupted after {duration_str}")
   850	            return
   851	
   852	        verb = random.choice(self._RUN_ELAPSED_VERBS)
   853	        self._renderer.append_elapsed_message(f"✻ {verb} in {duration_str}")
   854	
   855	    @staticmethod
   856	    def _is_lightweight_external_turn(
   857	        event: ExternalTurnScheduledEvent,
   858	    ) -> bool:
   859	        return (
   860	            str(event.source or "").strip() == "team_inbox"
   861	            and str(event.dispatch_reason or "").strip() == "idle_auto"
   862	        )
   863	
   864	    def _set_busy(self, value: bool) -> None:
   865	        was_busy = self._busy
   866	        self._busy = value
   867	        if value and not was_busy:
   868	            self._loading_tip_text = random.choice(LOADING_TIPS) if LOADING_TIPS else ""
   869	            self._loading_tip_show_at_monotonic = (
   870	                time.monotonic() + _LOADING_TIP_DELAY_SECONDS
   871	            )
   872	        elif not value:
   873	            self._loading_tip_text = ""
   874	            self._loading_tip_show_at_monotonic = None
   875	        self._render_dirty = True
   876	        self._invalidate()
   877	
   878	    async def _handle_error(self, exc: Exception) -> None:
   879	        """Unified error cleanup: format → display → stop animation → reset state."""
   880	        from comate_cli.terminal_agent.error_display import format_error
   881	
   882	        message, transient_summary, severity = format_error(exc)
   883	
   884	        self._renderer.append_system_message(message, severity=severity)
   885	        self._renderer.interrupt_turn()
   886	        await self._animation_controller.shutdown()
   887	        self._status_bar.show_transient(transient_summary, severity=severity)
   888	        self._last_turn_user_preview = None
   889	        self._append_run_elapsed_to_history(stop_reason="error")
   890	        self._run_start_time = None
   891	        self._interrupt_requested_at = None
   892	        self._set_busy(False)
   893	        await self._status_bar.refresh()
   894	        self._refresh_layers()
   895	
   896	    async def _submit_user_message(
   897	        self,
   898	        text: str,
   899	        *,
   900	        display_text: str | None = None,
   901	    ) -> None:
   902	        if self._busy:
   903	            self._renderer.append_system_message(
   904	                "当前已有任务在运行，请稍候。", severity="error"
   905	            )
   906	            return
   907	
   908	        if self._ui_mode == UIMode.QUESTION:
   909	            self._exit_question_mode()
   910	
   911	        self._session.run_controller.clear()
   912	        self._interrupt_requested_at = None
   913	
   914	        self._set_busy(True)
   915	        self._run_start_time = time.monotonic()
   916	        self._waiting_for_input = False
   917	        self._pending_questions = None
   918	        # 本轮默认 loading 文案：避免出现 Working… 这种突兀 fallback
   919	        self._fallback_loading_phrase = (
   920	            random.choice(DEFAULT_STATUS_PHRASES)
   921	            if DEFAULT_STATUS_PHRASES
   922	            else f"Thinking{ELLIPSIS}"
   923	        )
   924	        self._fallback_phrase_refresh_at = time.monotonic() + 3.0  # 每 3 秒允许换一次
   925	
   926	        self._renderer.start_turn()
   927	        self._renderer.seed_user_message(
   928	            display_text if display_text is not None else text
   929	        )
   930	        self._last_turn_user_preview = display_text if display_text is not None else text
   931	        self._refresh_layers()
   932	
   933	        # 启动提交动画
   934	        await self._animation_controller.start()
   935	
   936	        # @path context hint injection
   937	        try:
   938	            hint = build_path_context_hint(text, Path.cwd())
   939	            if hint is not None:
   940	                self._session._agent._context.add_system_reminder_message(hint)
   941	        except Exception:
   942	            logger.debug("path context hint injection failed", exc_info=True)
   943	
   944	        try:
   945	            await self._session.send(text)
   946	        except Exception as exc:
   947	            logger.exception("send failed")
   948	            await self._handle_error(exc)
   949	
   950	    async def _consume_event_stream(self) -> None:
   951	        waiting_for_input = False
   952	        questions: list[dict[str, Any]] | None = None
   953	        plan_approval: dict[str, str] | None = None
   954	        stop_reason: str | None = None
   955	        try:
   956	            async for event in self._session.events():
   957	                if self._closing:
   958	                    break
   959	                stop_error: Exception | None = None
   960	                self._maybe_cancel_tool_result_flash(event)
   961	                await self._animation_controller.on_event(event)
   962	                if isinstance(event, SessionInitEvent):
   963	                    self._initialized_session_id = str(event.session_id)
   964	                if isinstance(event, QueueDirtyEvent):
   965	                    if event.reason in {
   966	                        QueueDirtyReason.CANCELLED,
   967	                        QueueDirtyReason.CLEARED,
   968	                    }:
   969	                        for message_id in event.message_ids:
   970	                            self._queued_display_by_message_id.pop(
   971	                                str(message_id),
   972	                                None,
   973	                            )
   974	                    if event.reason is QueueDirtyReason.CONSUMED:
   975	                        consumed_human_displays: list[str] = []
   976	                        for message_id, origin in zip(event.message_ids, event.origins):
   977	                            if origin is not MessageOrigin.HUMAN:
   978	                                continue
   979	                            display_text = self._queued_display_by_message_id.pop(
   980	                                str(message_id),
   981	                                None,
   982	                            )
   983	                            if display_text:
   984	                                consumed_human_displays.append(display_text)
   985	                        if consumed_human_displays and not self._busy:
   986	                            self._session.run_controller.clear()
   987	                            self._interrupt_requested_at = None
   988	                            self._waiting_for_input = False
   989	                            self._pending_questions = None
   990	                            self._pending_plan_approval = None
   991	                            self._set_busy(True)
   992	                            self._run_start_time = time.monotonic()
   993	                            self._fallback_loading_phrase = (
   994	                                random.choice(DEFAULT_STATUS_PHRASES)
   995	                                if DEFAULT_STATUS_PHRASES
   996	                                else f"Thinking{ELLIPSIS}"
   997	                            )
   998	                            self._fallback_phrase_refresh_at = time.monotonic() + 3.0
   999	                            self._renderer.start_turn()
  1000	                            await self._animation_controller.start()
  1001	                            self._last_turn_user_preview = consumed_human_displays[-1]
  1002	                        for display_text in consumed_human_displays:
  1003	                            self._renderer.seed_user_message(display_text)
  1004	                    self._refresh_layers()
  1005	                    self._invalidate()
  1006	                if isinstance(event, QueuedMessageInjectedEvent):
  1007	                    for message_id, origin in zip(event.message_ids, event.origins):
  1008	                        if origin is not MessageOrigin.HUMAN:
  1009	                            continue
  1010	                        display_text = self._queued_display_by_message_id.pop(
  1011	                            str(message_id),
  1012	                            None,
  1013	                        )
  1014	                        if display_text:
  1015	                            self._renderer.seed_user_message(display_text)
  1016	                if isinstance(event, ExternalTurnScheduledEvent) and not self._busy:
  1017	                    self._session.run_controller.clear()
  1018	                    self._interrupt_requested_at = None
  1019	                    self._waiting_for_input = False
  1020	                    self._pending_questions = None
  1021	                    self._pending_plan_approval = None
  1022	                    if self._is_lightweight_external_turn(event):
  1023	                        self._background_turn_source = str(event.source or "").strip()
  1024	                    else:
  1025	                        self._set_busy(True)
  1026	                        self._run_start_time = time.monotonic()
  1027	                        self._renderer.start_turn()
  1028	                        await self._animation_controller.start()
  1029	                    self._refresh_layers()
  1030	                if isinstance(event, PlanApprovalRequiredEvent):
  1031	                    plan_approval = {
  1032	                        "plan_path": str(event.plan_path),
  1033	                        "summary": str(event.summary),
  1034	                        "execution_prompt": str(event.execution_prompt),
  1035	                        "context_utilization_pct": event.context_utilization_pct,
  1036	                    }
  1037	                if isinstance(event, StopEvent):
  1038	                    stop_reason = str(event.reason or "")
  1039	                    raw_stop_error = event.metadata.get("error")
  1040	                    if raw_stop_error is not None:
  1041	                        stop_error = (
  1042	                            raw_stop_error
  1043	                            if isinstance(raw_stop_error, Exception)
  1044	                            else Exception(str(raw_stop_error))
  1045	                        )
  1046	                        stop_reason = "error"
  1047	                    if event.reason == "shutdown_request":
  1048	                        shutdown_recipients = event.metadata.get("shutdown_recipients", [])
  1049	                        if shutdown_recipients:
  1050	                            from comate_agent_sdk.agent.runner_engine.query_stream import (
  1051	                                _send_team_shutdown_response,
  1052	                            )
  1053	                            _send_team_shutdown_response(
  1054	                                self._session.runtime, recipients=shutdown_recipients
  1055	                            )
  1056	                is_waiting, new_questions = self._renderer.handle_event(event)
  1057	                self._maybe_flash_tool_result(event)
  1058	                if is_waiting:
  1059	                    waiting_for_input = True
  1060	                    if new_questions is not None:
  1061	                        questions = new_questions
  1062	                if stop_error is not None:
  1063	                    from comate_cli.terminal_agent.error_display import format_error
  1064	
  1065	                    message, transient_summary, severity = format_error(stop_error)
  1066	                    self._renderer.append_system_message(message, severity=severity)
  1067	                    self._status_bar.show_transient(
  1068	                        transient_summary,
  1069	                        severity=severity,
  1070	                    )
  1071	                self._refresh_layers()
  1072	                if isinstance(event, StopEvent):
  1073	                    background_turn = self._background_turn_source is not None
  1074	                    # spec §6.4：finalize_turn 内会调 _force_flush_all 把
  1075	                    # held / container / text_pending / thinking 全部入队，
  1076	                    # 后续 _drain_history_async（同 frame）写 scrollback。
  1077	                    self._renderer.finalize_turn()
  1078	                    if not background_turn:
  1079	                        await self._animation_controller.shutdown()
  1080	                    self._last_turn_user_preview = None
  1081	                    self._interrupt_requested_at = None
  1082	                    if not background_turn:
  1083	                        self._append_run_elapsed_to_history(stop_reason=stop_reason)
  1084	                        self._run_start_time = None
  1085	                        self._set_busy(False)
  1086	                        await self._status_bar.refresh()
  1087	                    self._background_turn_source = None
  1088	
  1089	                    if waiting_for_input:
  1090	                        self._waiting_for_input = True
  1091	                        self._pending_questions = questions
  1092	                        self._pending_plan_approval = None
  1093	                        if questions:
  1094	                            self._enter_question_mode(questions)
  1095	                        else:
  1096	                            self._renderer.append_system_message(
  1097	                                "请输入对上述问题的回答后回车提交。"
  1098	                            )
  1099	                    elif plan_approval is not None:
  1100	                        self._waiting_for_input = False
  1101	                        self._pending_questions = None
  1102	                        self._pending_plan_approval = plan_approval
  1103	                        self._exit_question_mode()
  1104	                        self._open_plan_approval_menu(plan_approval)
  1105	                    else:
  1106	                        self._waiting_for_input = False
  1107	                        self._pending_questions = None
  1108	                        self._pending_plan_approval = None
  1109	                        self._exit_question_mode()
  1110	
  1111	                    self._refresh_layers()
  1112	
  1113	                    waiting_for_input = False
  1114	                    questions = None
  1115	                    plan_approval = None
  1116	                    stop_reason = None
  1117	        except asyncio.CancelledError:
  1118	            raise
  1119	        except Exception as exc:
  1120	            logger.exception("session event pump failed")
  1121	            await self._handle_error(exc)
  1122	
  1123	    def _open_plan_approval_menu(self, approval: dict[str, str]) -> None:
  1124	        plan_path = str(approval.get("plan_path", "")).strip()
  1125	        summary = str(approval.get("summary", "")).strip()
  1126	        execution_prompt = str(approval.get("execution_prompt", "")).strip()
  1127	        context_utilization_pct = int(approval.get("context_utilization_pct", 0))
  1128	        clear_label = (
  1129	            f"Yes, clear context ({context_utilization_pct}% used) and execute"
  1130	        )
  1131	
  1132	        options = [
  1133	             {
  1134	                "value": "approve_clear_execute",
  1135	                "label": clear_label,
  1136	                "description": "Clear conversation history, then switch to Act Mode and execute.",
  1137	            },
  1138	            {
  1139	                "value": "approve_execute",
  1140	                "label": "Approve and execute",
  1141	                "description": "Switch to Act Mode and execute immediately.",
  1142	            },
  1143	           
  1144	            {
  1145	                "value": "reject_continue",
  1146	                "label": "Reject and continue planning",
  1147	                "description": "Stay in Plan Mode and continue refining the plan.",
  1148	            },
  1149	        ]
  1150	
  1151	        title = "Plan approval required"
  1152	        if summary:
  1153	            title = f"Plan approval: {summary}"
  1154	
  1155	        def on_confirm(value: str) -> None:
  1156	            if value == "approve_execute":
  1157	                self._schedule_background(
  1158	                    self._approve_plan_and_execute(
  1159	                        plan_path=plan_path,
  1160	                        execution_prompt=execution_prompt,
  1161	                        clear_context=False,
  1162	                    )
  1163	                )
  1164	                return
  1165	            if value == "approve_clear_execute":
  1166	                self._schedule_background(
  1167	                    self._approve_plan_and_execute(
  1168	                        plan_path=plan_path,
  1169	                        execution_prompt=execution_prompt,
  1170	                        clear_context=True,
  1171	                    )
  1172	                )
  1173	                return
  1174	            if value == "reject_continue":
  1175	                self._reject_plan_and_continue(plan_path=plan_path)
  1176	
  1177	        def on_cancel() -> None:
  1178	            self._reject_plan_and_continue(plan_path=plan_path)
  1179	
  1180	        ok = self._selection_ui.set_options(
  1181	            title=title,
  1182	            options=options,
  1183	            on_confirm=on_confirm,
  1184	            on_cancel=on_cancel,
  1185	        )
  1186	        if not ok:
  1187	            self._renderer.append_system_message(
  1188	                "No plan approval options available.",
  1189	                severity="error",
  1190	            )
  1191	            return
  1192	        self._selection_ui.refresh()
  1193	        self._ui_mode = UIMode.SELECTION
  1194	        self._sync_focus_for_mode()
  1195	        self._invalidate()
  1196	
  1197	    async def _approve_plan_and_execute(
  1198	        self,
  1199	        *,
  1200	        plan_path: str,
  1201	        execution_prompt: str,
  1202	        clear_context: bool = False,
  1203	    ) -> None:
  1204	        prompt = self._session.approve_plan(clear_context=clear_context)
  1205	        if execution_prompt.strip():
  1206	            prompt = execution_prompt.strip()
  1207	        self._pending_plan_approval = None
  1208	        try:
  1209	            self._status_bar.set_mode(self._session.get_mode())
  1210	        except Exception:
  1211	            pass
  1212	        self._renderer.append_system_message(f"Plan approved: {plan_path}")
  1213	        self._refresh_layers()
  1214	        await self._submit_user_message(prompt, display_text="Execute approved plan")
  1215	
  1216	    def _reject_plan_and_continue(self, *, plan_path: str) -> None:
  1217	        self._session.reject_plan()
  1218	        self._pending_plan_approval = None
  1219	        try:
  1220	            self._status_bar.set_mode(self._session.get_mode())
  1221	        except Exception:
  1222	            pass
  1223	        self._renderer.append_system_message(f"Plan rejected: {plan_path}")
  1224	        self._refresh_layers()
  1225	
  1226	    def _maybe_cancel_tool_result_flash(self, event: object) -> None:
  1227	        if not self._tool_result_flash_active:
  1228	            return
  1229	
  1230	        from comate_agent_sdk.agent.events import StopEvent, TextEvent
  1231	
  1232	        if isinstance(event, TextEvent) or isinstance(event, StopEvent):
  1233	            self._schedule_background(self._stop_tool_result_flash())
  1234	
  1235	    def _maybe_flash_tool_result(self, event: object) -> None:
  1236	        from comate_agent_sdk.agent.events import ToolResultEvent
  1237	
  1238	        if not isinstance(event, ToolResultEvent):
  1239	            return
  1240	        tool_name = str(event.tool or "").strip()
  1241	        if tool_name.lower() == "todowrite":
  1242	            return
  1243	        if self._renderer.has_running_tools():
  1244	            return
  1245	        self._trigger_tool_result_flash(
  1246	            tool_name=tool_name or "Tool",
  1247	            is_error=bool(event.is_error),
  1248	        )
  1249	
  1250	    def _trigger_tool_result_flash(self, *, tool_name: str, is_error: bool) -> None:
  1251	        del is_error, tool_name
  1252	        phrase = (
  1253	            random.choice(DEFAULT_STATUS_PHRASES)
  1254	            if DEFAULT_STATUS_PHRASES
  1255	            else f"Embellishing{ELLIPSIS}"
  1256	        )
  1257	        duration_seconds = max(0.15, float(self._tool_result_flash_seconds))
  1258	        self._tool_result_flash_gen += 1
  1259	        self._tool_result_flash_active = True
  1260	        self._tool_result_flash_until_monotonic = time.monotonic() + duration_seconds
  1261	        gen = self._tool_result_flash_gen
  1262	        self._schedule_background(self._start_tool_result_flash(gen=gen, hint=phrase))
  1263	
  1264	    async def _start_tool_result_flash(self, *, gen: int, hint: str) -> None:
  1265	        if gen != self._tool_result_flash_gen:
  1266	            return
  1267	        await self._tool_result_animator.start()
  1268	        if gen != self._tool_result_flash_gen:
  1269	            return
  1270	        self._tool_result_animator.set_status_hint(hint)
  1271	        self._render_dirty = True
  1272	        self._invalidate()
  1273	
  1274	    async def _stop_tool_result_flash(self) -> None:
  1275	        if not self._tool_result_flash_active:
  1276	            return
  1277	        self._tool_result_flash_gen += 1
  1278	        self._tool_result_flash_active = False
  1279	        self._tool_result_flash_until_monotonic = None
  1280	        self._tool_result_animator.set_status_hint(None)
  1281	        await self._tool_result_animator.stop()
  1282	        self._render_dirty = True
  1283	        self._invalidate()
  1284	
  1285	    def _schedule_background(self, coroutine: Any) -> None:
  1286	        task = asyncio.create_task(coroutine)
  1287	
  1288	        def _done(done_task: asyncio.Task[Any]) -> None:
  1289	            try:
  1290	                done_task.result()
  1291	            except asyncio.CancelledError:
  1292	                return
  1293	            except Exception:
  1294	                logger.exception("background task failed")
  1295	
  1296	        task.add_done_callback(_done)
  1297	
  1298	    def _refresh_layers(self) -> None:
  1299	        self._sync_focus_for_mode()
  1300	        self._render_dirty = True
  1301	
  1302	    def _fetch_tasks_from_store(self) -> tuple[list[dict], str] | None:
  1303	        """从 TaskStore 读取最新 task 列表（纯 I/O，线程安全）。
  1304	
  1305	        返回 (task_dicts, list_id) 或 None。
  1306	        不修改任何共享状态——状态更新由调用方在主线程完成。
  1307	        """
  1308	        try:
  1309	            agent = getattr(self._session, "_agent", None)
  1310	            if agent is None:
  1311	                return None
  1312	            store = getattr(agent, "_task_store", None)
  1313	            if store is None:
  1314	                return None
  1315	            tasks = store.list_tasks()
  1316	            if not tasks:
  1317	                return None
  1318	            # 转换 TaskItem 为 dict（与 chat_session.py 格式一致）
  1319	            task_dicts: list[dict] = []
  1320	            for task in tasks:
  1321	                payload = task.model_dump(mode="json")
  1322	                payload["open_blocked_by"] = store.get_open_blocked_by(task.id)
  1323	                task_dicts.append(payload)
  1324	            list_id = store.list_id_value()
  1325	            return task_dicts, list_id
  1326	        except Exception:
  1327	            logger.debug("task store fetch failed", exc_info=True)
  1328	            return None
  1329	

<system-reminder>
Recommended next step:
* Read(file_path="/home/hyc/projects/agent-sdk/packages/comate_cli/comate_cli/terminal_agent/tui.py", offset=1330, limit=700)
* Grep(pattern="<keywords>", path="/home/hyc/projects/agent-sdk/packages/comate_cli/comate_cli/terminal_agent/tui.py", output_mode="content")
</system-reminder>