```
import random
import threading
import time
from collections import defaultdict
from enum import StrEnum
from typing import Callable, Optional

import flet as ft
from pydantic import BaseModel, Field
from uuid6 import uuid7


# ──────────────────────────── Event Topics ────────────────────────────────────
class Topic(StrEnum):
    # Application-wide state
    APP_STATE_CHANGED = "app.state.changed"

    # Chat / processing life-cycle
    CHAT_MSG_SEND = "chat.msg.send"
    CHAT_PROCESSING_START = "chat.processing.start"
    CHAT_PROCESSING_FINISH = "chat.processing.finish"
    CHAT_PROCESSING_CANCEL = "chat.processing.cancel"

    # Tree / node editing
    TREE_NODE_UPDATE = "tree.node.update"
    TREE_NODE_ADDED = "tree.node.added"
    TREE_SIBLING_NEXT = "tree.sibling.next"
    TREE_SIBLING_PREV = "tree.sibling.prev"


# ────────────────────────────── Domain Layer ──────────────────────────────────
class TreeNode(BaseModel):
    id: str = Field(default_factory=lambda: str(uuid7()))
    role: str
    reasoning: str
    response: str
    children: list["TreeNode"] = Field(default_factory=list)
    active_index: int = 0

    @property
    def active_child(self) -> Optional["TreeNode"]:
        if not self.children:
            return None
        self._clamp_index()
        return self.children[self.active_index]

    def next_child(self) -> None:
        if self.children:
            self.active_index = min(self.active_index + 1, len(self.children) - 1)

    def prev_child(self) -> None:
        if self.children:
            self.active_index = max(self.active_index - 1, 0)

    def _clamp_index(self) -> None:
        self.active_index = min(max(self.active_index, 0), len(self.children) - 1)


class Tree(BaseModel):
    root_nodes: list[TreeNode] = Field(default_factory=list)
    active_tree_index: int = 0

    def active_path(self) -> list[TreeNode]:
        if not self.root_nodes:
            return []
        self.active_tree_index = min(
            max(self.active_tree_index, 0), len(self.root_nodes) - 1
        )
        path: list[TreeNode] = []
        node: Optional[TreeNode] = self.root_nodes[self.active_tree_index]
        while node:
            path.append(node)
            node = node.active_child
        return path


# ───────────────────────────── Event Bus ──────────────────────────────────────
class EventBus:
    def __init__(self) -> None:
        self._subs: dict[str, list[Callable]] = defaultdict(list)

    def sub(self, topic: Topic, fn: Callable) -> None:
        self._subs[topic].append(fn)

    def pub(self, topic: Topic, data=None) -> None:
        for fn in self._subs.get(topic, []):
            fn(data)


# ──────────────────────────── Application Service ─────────────────────────────
class TreeService:
    def __init__(self, tree: Tree, bus: EventBus) -> None:
        self._tree = tree
        self._bus = bus
        self._on_user_input: Optional[Callable[[TreeNode], None]] = None
        self._cancel_flag = False
        self._is_processing = False

        # イベント購読
        self._bus.sub(Topic.TREE_NODE_UPDATE, self._handle_update_node)
        self._bus.sub(Topic.TREE_SIBLING_NEXT, self._handle_next_sibling)
        self._bus.sub(Topic.TREE_SIBLING_PREV, self._handle_prev_sibling)
        self._bus.sub(Topic.CHAT_MSG_SEND, self._handle_send_message)
        self._bus.sub(Topic.CHAT_PROCESSING_CANCEL, lambda _: self.cancel_processing())

    def set_on_user_input(self, callback: Callable[[TreeNode], None]) -> None:
        self._on_user_input = callback

    # ─────────── event handlers ───────────
    def _handle_update_node(self, data: dict[str, str]) -> None:
        node_id = data["node_id"]
        node = self._find_node_by_id(node_id)
        if node:
            node.reasoning = data["reasoning"]
            node.reasoning = data["reasoning"]
            node.response = data["response"]
            self._bus.pub(Topic.APP_STATE_CHANGED)

    def _handle_next_sibling(self, parent_id: str) -> None:
        parent = self._find_node_by_id(parent_id)
        if parent:
            parent.next_child()
            self._bus.pub(Topic.APP_STATE_CHANGED)

    def _handle_prev_sibling(self, parent_id: str) -> None:
        parent = self._find_node_by_id(parent_id)
        if parent:
            parent.prev_child()
            self._bus.pub(Topic.APP_STATE_CHANGED)

    def _handle_send_message(self, message: str) -> None:
        if self._should_ignore_message(message):
            return

        user_node = self._add_user_node(message.strip())
        self._bus.pub(Topic.APP_STATE_CHANGED)
        self._start_processing(user_node)

    def _should_ignore_message(self, message: str) -> bool:
        return self._is_processing or not message.strip()

    def _add_user_node(self, message: str) -> TreeNode:
        user_node = TreeNode(role="user", reasoning="", response=message)
        path = self._tree.active_path()

        if path:
            parent = path[-1]
            parent.children.append(user_node)
            parent.active_index = len(parent.children) - 1
        else:
            self._tree.root_nodes.append(user_node)

        self._bus.pub(Topic.TREE_NODE_ADDED)
        return user_node

    def _start_processing(self, user_node: TreeNode) -> None:
        if not self._on_user_input:
            return

        self._is_processing = True
        self._cancel_flag = False
        self._bus.pub(Topic.CHAT_PROCESSING_START)

        thread = threading.Thread(
            target=self._run_user_input_handler,
            args=(user_node,),
            daemon=True
        )
        thread.start()

    # ─────────── processing control ───────────
    def cancel_processing(self) -> None:
        self._cancel_flag = True

    def _run_user_input_handler(self, user_node: TreeNode) -> None:
        try:
            self._on_user_input(user_node)
        finally:
            self._is_processing = False
            self._bus.pub(Topic.CHAT_PROCESSING_FINISH)

    # ─────────── helpers ───────────
    def _find_node_by_id(
            self, node_id: str, node: Optional[TreeNode] = None
    ) -> Optional[TreeNode]:
        if node is None:
            for root in self._tree.root_nodes:
                found = self._find_node_by_id(node_id, root)
                if found:
                    return found
            return None
        if node.id == node_id:
            return node
        for child in node.children:
            found = self._find_node_by_id(node_id, child)
            if found:
                return found
        return None

    # ─────────── properties ───────────
    @property
    def is_processing(self) -> bool:
        return self._is_processing

    @property
    def cancel_flag(self) -> bool:
        return self._cancel_flag


# ──────────────────────────────── UI Layer ────────────────────────────────────
class ChatCard(ft.Card):
    def __init__(
            self,
            node: TreeNode,
            parent_node: Optional[TreeNode],
            bus: EventBus,
            *args,
            **kwargs,
    ):
        self._node = node
        self._parent = parent_node
        self._bus = bus
        self._edit_mode = False
        self._show_reasoning = False
        self._original_reasoning = ""
        self._original_response = ""
        self.reasoning_tf = ft.TextField(label="Reasoning", multiline=True, expand=True)
        self.response_tf = ft.TextField(label="Response", multiline=True, expand=True)
        super().__init__(content=self._with_padding(self._build_content()), *args, **kwargs)

    # ─────────── build helpers ───────────
    def _with_padding(self, content: ft.Control) -> ft.Control:
        return ft.Container(content=content, padding=16)

    def _build_header(self) -> ft.Row:
        title = ft.Text(self._node.role, style=ft.TextThemeStyle.TITLE_SMALL)
        controls: list[ft.Control] = [title, ft.Container(expand=True)]
        if not self._edit_mode:
            reasoning_btn = ft.IconButton(
                icon=ft.Icons.PSYCHOLOGY,
                icon_color=ft.Colors.BLUE if self._show_reasoning else None,
                tooltip="Reasoningを隠す" if self._show_reasoning else "Reasoningを表示",
                on_click=self._toggle_reasoning,
                disabled=not self._node.reasoning.strip(),
            )
            controls.append(reasoning_btn)
        edit_btn = ft.IconButton(
            icon=ft.Icons.EDIT,
            icon_color=ft.Colors.RED if self._edit_mode else None,
            tooltip="キャンセル編集" if self._edit_mode else "編集モードへ",
            on_click=self._cancel_and_exit if self._edit_mode else self._enter_edit_mode,
        )
        controls.append(edit_btn)
        controls.extend(self._build_nav_controls())
        return ft.Row(controls, alignment=ft.MainAxisAlignment.START)

    def _build_nav_controls(self) -> list[ft.Control]:
        if not (self._parent and len(self._parent.children) > 1):
            return []
        index = self._parent.children.index(self._node)
        total = len(self._parent.children)
        return [
            ft.IconButton(
                icon=ft.Icons.CHEVRON_LEFT,
                on_click=lambda _: self._bus.pub(Topic.TREE_SIBLING_PREV, self._parent.id),
                disabled=index == 0,
                tooltip="前の兄弟ノードへ",
            ),
            ft.Text(f"{index + 1}/{total}", style=ft.TextThemeStyle.BODY_SMALL),
            ft.IconButton(
                icon=ft.Icons.CHEVRON_RIGHT,
                on_click=lambda _: self._bus.pub(Topic.TREE_SIBLING_NEXT, self._parent.id),
                disabled=index >= total - 1,
                tooltip="次の兄弟ノードへ",
            ),
        ]

    def _build_edit_controls(self) -> ft.Column:
        buttons = ft.Row(
            [
                ft.Container(expand=True),
                ft.OutlinedButton("キャンセル", on_click=self._cancel_and_exit),
                ft.FilledButton("更新", on_click=self._update_and_exit),
            ],
            spacing=8,
        )
        return ft.Column(
            [self._build_header(), self.reasoning_tf, self.response_tf, buttons], spacing=8
        )

    def _build_display_controls(self) -> ft.Column:
        controls: list[ft.Control] = [self._build_header()]
        if self._show_reasoning and self._node.reasoning.strip():
            controls.append(
                ft.Text(
                    value=self._node.reasoning,
                    style=ft.TextThemeStyle.BODY_MEDIUM,
                    selectable=True,
                    color=ft.Colors.BLUE_GREY_600,
                )
            )
        if self._node.response:
            controls.append(
                ft.Text(
                    value=self._node.response,
                    style=ft.TextThemeStyle.BODY_MEDIUM,
                    selectable=True,
                )
            )
        return ft.Column(controls, spacing=4)

    def _build_content(self) -> ft.Control:
        return self._build_edit_controls() if self._edit_mode else self._build_display_controls()

    # ─────────── UI callbacks ───────────
    def _toggle_reasoning(self, _) -> None:
        self._show_reasoning = not self._show_reasoning
        self._rebuild()

    def _enter_edit_mode(self, _) -> None:
        self._edit_mode = True
        self._original_reasoning = self._node.reasoning
        self._original_response = self._node.response
        self.reasoning_tf.value = self._node.reasoning
        self.response_tf.value = self._node.response
        self._rebuild()

    def _update_and_exit(self, _) -> None:
        self._bus.pub(
            Topic.TREE_NODE_UPDATE,
            {
                "node_id": self._node.id,
                "reasoning": self.reasoning_tf.value,
                "response": self.response_tf.value,
            },
        )
        self._edit_mode = False

    def _cancel_and_exit(self, _) -> None:
        self._edit_mode = False
        self.reasoning_tf.value = self._original_reasoning
        self.response_tf.value = self._original_response
        self._rebuild()

    def _rebuild(self) -> None:
        self.content = self._with_padding(self._build_content())
        self.update()


# ──────────────────────────── Demo Processing ─────────────────────────────────
def create_demo_processor(service: TreeService, bus: EventBus) -> Callable[[TreeNode], None]:
    __lipsum__ = [
        "lorem",
        "ipsum",
        "dolor",
        "sit",
        "amet",
        "consectetur",
        "adipiscing",
        "elit",
        "sed",
        "do",
        "eiusmod",
        "tempor",
        "incididunt",
        "ut",
        "labore",
        "et",
        "dolore",
        "magna",
        "aliqua",
        "enim",
        "ad",
        "minim",
        "veniam",
        "quis",
        "nostrud",
        "exercitation",
        "ullamco",
        "laboris",
        "nisi",
        "aliquip",
        "ex",
        "ea",
        "commodo",
        "consequat",
        "duis",
        "aute",
        "irure",
        "reprehenderit",
        "in",
        "voluptate",
        "velit",
        "esse",
        "cillum",
        "fugiat",
        "nulla",
        "pariatur",
        "excepteur",
        "sint",
        "occaecat",
        "cupidatat",
        "non",
        "proident",
        "sunt",
        "culpa",
        "qui",
        "officia",
        "deserunt",
        "mollit",
        "anim",
        "id",
        "est",
        "laborum",
    ]

    def process_user_input(user_node: TreeNode) -> None:
        assistant_node = TreeNode(role="assistant", reasoning="", response="")
        user_node.children.append(assistant_node)
        user_node.active_index = 0
        bus.pub(Topic.TREE_NODE_ADDED)
        bus.pub(Topic.APP_STATE_CHANGED)
        words = []
        time.sleep(2)
        for _ in range(random.randint(10, 30)):
            if service.cancel_flag:
                break
            words.append(random.choice(__lipsum__))
            assistant_node.response = " ".join(words).capitalize()
            bus.pub(Topic.APP_STATE_CHANGED)
            time.sleep(0.5)
        if not service.cancel_flag and assistant_node.response:
            assistant_node.response += "."
            bus.pub(Topic.APP_STATE_CHANGED)

    return process_user_input


# ────────────────────────── Demo Tree Constructor ─────────────────────────────
def build_demo_tree() -> Tree:
    root = TreeNode(
        role="system", reasoning="", response="こんにちは！何かお手伝いできることはありますか？"
    )
    a = TreeNode(role="user", reasoning="", response="明日の天気は？")
    b = TreeNode(role="user", reasoning="", response="東京への行き方は？")
    c = TreeNode(role="user", reasoning="", response="おでんの具で一番美味しいのは？")
    root.children, root.active_index = [a, b, c], 1
    b1 = TreeNode(
        role="assistant",
        reasoning="[東京への行き方を考える]",
        response="すみません、よくわかりませんでした。",
    )
    b2 = TreeNode(
        role="assistant",
        reasoning="[東京への行き方を考える]",
        response="好きに行けばいいと思います。",
    )
    b.children = [b1, b2]
    return Tree(root_nodes=[root])


# ─────────────────────────────── Chat Page ────────────────────────────────────
class ChatPage(ft.Container):
    def __init__(self, tree: Tree, bus: EventBus) -> None:
        super().__init__(expand=True)
        self.tree = tree
        self.bus = bus
        self._is_processing = False
        self.bus.sub(Topic.TREE_NODE_ADDED, self.scroll_to_bottom)

        self.chat_list = ft.ListView(expand=True, spacing=12, controls=[])
        self.message_input = ft.TextField(
            label="送信するメッセージ",
            expand=True,
            on_submit=self.send_message,
            multiline=True,
            min_lines=1,
            max_lines=5,
        )
        self.action_button = ft.IconButton(
            icon=ft.Icons.SEND_ROUNDED, tooltip="メッセージを送る", on_click=self.send_message
        )

        self.chat_sender = ft.Column(
            controls=[ft.Row([self.message_input, self.action_button])]
        )
        self.content = ft.Column(
            expand=True,
            controls=[ft.Container(content=self.chat_list, expand=True), self.chat_sender],
        )

        # バス登録
        self.bus.sub(Topic.CHAT_PROCESSING_START, self._on_processing_start)
        self.bus.sub(Topic.CHAT_PROCESSING_FINISH, self._on_processing_finish)
        self.bus.sub(Topic.APP_STATE_CHANGED, self.rebuild_chat_list)

    # ─────────── lifecycle ───────────
    def did_mount(self) -> None:
        self.rebuild_chat_list()

    # ─────────── UI updates ───────────
    def rebuild_chat_list(self, _=None) -> None:
        self.chat_list.controls.clear()
        path = self.tree.active_path()
        for depth, node in enumerate(path):
            parent = path[depth - 1] if depth > 0 else None
            self.chat_list.controls.append(ChatCard(node=node, parent_node=parent, bus=self.bus))
        self.update()

    def scroll_to_bottom(self, _=None):
        if self.chat_list.controls:
            self.chat_list.scroll_to(offset=-1, duration=500, curve=ft.AnimationCurve.EASE_IN_OUT)
            self.update()

    # ─────────── send / cancel ───────────
    def send_message(self, _) -> None:
        if self._is_processing:
            self.bus.pub(Topic.CHAT_PROCESSING_CANCEL)
        else:
            message = self.message_input.value
            if message and message.strip():
                self.bus.pub(Topic.CHAT_MSG_SEND, message)
                self.message_input.value = ""
                self.message_input.focus()
        self.update()

    # ─────────── processing state ───────────
    def _on_processing_start(self, _=None) -> None:
        self._is_processing = True
        self.action_button.icon = ft.Icons.CANCEL
        self.action_button.tooltip = "処理をキャンセル"
        self.message_input.disabled = True
        self.update()

    def _on_processing_finish(self, _=None) -> None:
        self._is_processing = False
        self.action_button.icon = ft.Icons.SEND_ROUNDED
        self.action_button.tooltip = "メッセージを送る"
        self.message_input.disabled = False
        self.message_input.focus()
        self.update()


# ──────────────────────────────── Flet Main ───────────────────────────────────
def main(page: ft.Page) -> None:
    page.title = "TreeNode Demo"
    bus = EventBus()
    tree = build_demo_tree()

    service = TreeService(tree, bus)
    demo_processor = create_demo_processor(service, bus)
    service.set_on_user_input(demo_processor)

    chat_page = ChatPage(tree=tree, bus=bus)
    page.add(chat_page)

    bus.pub(Topic.APP_STATE_CHANGED)


if __name__ == "__main__":
    ft.app(target=main)
```

Python 3.12+, Flet 0.28.3.
APP_STATE_CHANGEDというイベントはざっくりしすぎているので、より細分化して無駄をなくして。