```
# tree_app_refactored.py
# Python 3.12+ / Flet 0.28.3

from __future__ import annotations

import uuid
from collections import defaultdict
from typing import Callable, List, Optional

import flet as ft
from pydantic import BaseModel, Field


# ────────────────────────────── Domain layer ──────────────────────────────────
class TreeNode(BaseModel):
    """
    Single conversation unit.

    • `reasoning` : chain-of-thought
    • `response`  : final text shown to user
    • `children`  : next turns of the same branch

    UI とは無関係に階層・ナビゲーションを管理できるように振る舞いを持たせる。
    """

    id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    role: str
    reasoning: str
    response: str
    children: List["TreeNode"] = Field(default_factory=list)
    active_index: int = 0  # currently selected child

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

    # navigation behaviour kept inside the entity
    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)

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


class Tree(BaseModel):
    """
    Forest-level aggregate root.

    1. どの root が選択されているか (`active_tree_index`)
    2. root → active_child → … と辿った “現在のパス” を返す API がある
    """

    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:
    """
    Very small publish/subscribe hub.
    No threading: every callback runs synchronously on the UI thread.
    """

    def __init__(self) -> None:
        self._subs: dict[str, list[Callable]] = defaultdict(list)

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

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


# ──────────────────────────── Application service ─────────────────────────────
class TreeService:
    """
    Single façade the UI talks to.
    Encapsulates *all* state changes and notifies the rest of the app.
    """

    def __init__(self, tree: Tree, bus: EventBus) -> None:
        self._tree = tree
        self._bus = bus

    # ----- mutation API -------------------------------------------------------
    def update_text(
        self, node: TreeNode, reasoning: str, response: str, *, snack_msg=True
    ) -> None:
        node.reasoning, node.response = reasoning, response
        self._bus.pub("state_changed")
        if snack_msg:
            self._bus.pub("snackbar", "ノードを更新しました")

    def next_sibling(self, parent: TreeNode) -> None:
        parent.next_child()
        self._bus.pub("state_changed")

    def prev_sibling(self, parent: TreeNode) -> None:
        parent.prev_child()
        self._bus.pub("state_changed")

    # ----- query API ----------------------------------------------------------
    def active_path(self) -> List[TreeNode]:
        return self._tree.active_path()


# ──────────────────────────────── UI layer ────────────────────────────────────
class ChatCard(ft.Card):
    """Editable view for a single TreeNode."""

    def __init__(
        self,
        node: TreeNode,
        parent_node: Optional[TreeNode],
        service: TreeService,
        *args,
        **kwargs,
    ):
        self._node = node
        self._parent = parent_node
        self._svc = service

        # ---------- header (role + navigation) --------------------------------
        header_controls: list[ft.Control] = [
            ft.Text(value=node.role, style=ft.TextThemeStyle.TITLE_SMALL)
        ]

        if parent_node and len(parent_node.children) > 1:
            left_btn = ft.IconButton(
                icon=ft.Icons.CHEVRON_LEFT,
                on_click=lambda _: self._svc.prev_sibling(parent_node),
                disabled=parent_node.active_index == 0,
                tooltip="前の兄弟ノードへ",
            )
            right_btn = ft.IconButton(
                icon=ft.Icons.CHEVRON_RIGHT,
                on_click=lambda _: self._svc.next_sibling(parent_node),
                disabled=parent_node.active_index >= len(parent_node.children) - 1,
                tooltip="次の兄弟ノードへ",
            )
            header_controls.extend(
                [
                    ft.Container(expand=True),  # spacer
                    left_btn,
                    ft.Text(
                        f"{parent_node.active_index + 1}/{len(parent_node.children)}",
                        style=ft.TextThemeStyle.BODY_SMALL,
                    ),
                    right_btn,
                ]
            )

        header = ft.Row(header_controls, alignment=ft.MainAxisAlignment.START)

        # ---------- editors ----------------------------------------------------
        self.reasoning_tf = ft.TextField(
            value=node.reasoning, label="Reasoning", multiline=True, expand=True
        )
        self.response_tf = ft.TextField(
            value=node.response, label="Response", multiline=True, expand=True
        )

        update_btn = ft.FilledButton(
            text="更新",
            on_click=lambda _: self._svc.update_text(
                node, self.reasoning_tf.value, self.response_tf.value
            ),
        )

        body = ft.Column([header, self.reasoning_tf, self.response_tf, update_btn])
        super().__init__(content=body, *args, **kwargs)


class NodeContainer(ft.Container):
    """Wraps a ChatCard and gives a depth-dependent background tint."""

    def __init__(
        self,
        node: TreeNode,
        parent_node: Optional[TreeNode],
        service: TreeService,
        depth: int,
    ):
        super().__init__(
            content=ChatCard(node=node, parent_node=parent_node, service=service),
            padding=10,
            bgcolor=ft.Colors.with_opacity(0.05 + depth * 0.05, ft.Colors.BLUE_500),
            border_radius=8,
            expand=True,
        )


# ────────────────────────── Demo tree constructor ─────────────────────────────
def build_demo_tree() -> Tree:
    root = TreeNode(role="system", reasoning="root 1", response="resp 1")

    a = TreeNode(role="user", reasoning="A", response="a")
    b = TreeNode(role="assistant", reasoning="B", response="b")
    c = TreeNode(role="user", reasoning="C", response="c")
    root.children = [a, b, c]
    root.active_index = 1  # 'b' is selected

    b1 = TreeNode(role="user", reasoning="B1", response="b1")
    b2 = TreeNode(role="assistant", reasoning="B2", response="b2")
    b.children = [b1, b2]

    return Tree(root_nodes=[root])


# ──────────────────────────────── Flet main ───────────────────────────────────
def main(page: ft.Page) -> None:
    page.title = "TreeNode Active Path (decoupled)"
    page.scroll = ft.ScrollMode.AUTO

    bus = EventBus()
    service = TreeService(build_demo_tree(), bus)

    body_column = ft.Column(spacing=12)
    page.add(body_column)

    # ---------- rebuild UI whenever the state changes -------------------------
    def rebuild_body(_=None):
        body_column.controls.clear()
        path = service.active_path()
        for depth, node in enumerate(path):
            parent = path[depth - 1] if depth > 0 else None
            body_column.controls.append(
                NodeContainer(node=node, parent_node=parent, service=service, depth=depth)
            )
        page.update()

    bus.sub("state_changed", rebuild_body)
    rebuild_body()  # initial render

    # ---------- snackbar listener --------------------------------------------
    def show_snackbar(msg: str):
        page.snack_bar = ft.SnackBar(content=ft.Text(msg), open=True)
        page.update()

    bus.sub("snackbar", show_snackbar)


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

Python 3.12-13, Flet 0.28.3.
現在、ノードをコンテナにラップしているが、これはあまり意味がないので、ツリー全体をラップするように書き換えて。また、色を変える機能は不要なのでオミットして。