```
# 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 with display/edit mode toggle."""

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

        # TextFields for edit mode
        self.reasoning_tf = ft.TextField(
            label="Reasoning", multiline=True, expand=True
        )
        self.response_tf = ft.TextField(
            label="Response", multiline=True, expand=True
        )

        # Build initial content
        content = self._build_content()
        super().__init__(content=content, *args, **kwargs)

    def _build_content(self) -> ft.Control:
        # Header with role and controls
        header_controls: list[ft.Control] = [
            ft.Text(value=self._node.role, style=ft.TextThemeStyle.TITLE_SMALL), ft.Container(expand=True)
        ]

        # Edit mode toggle button (placed before navigation arrows)
        if not self._edit_mode:
            edit_btn = ft.IconButton(
                icon=ft.Icons.EDIT,
                on_click=self._enter_edit_mode,
                tooltip="編集モード",
            )
            header_controls.append(edit_btn)

        # Navigation arrows for siblings
        if self._parent and len(self._parent.children) > 1:
            left_btn = ft.IconButton(
                icon=ft.Icons.CHEVRON_LEFT,
                on_click=lambda _: self._svc.prev_sibling(self._parent),
                disabled=self._parent.active_index == 0,
                tooltip="前の兄弟ノードへ",
            )
            right_btn = ft.IconButton(
                icon=ft.Icons.CHEVRON_RIGHT,
                on_click=lambda _: self._svc.next_sibling(self._parent),
                disabled=self._parent.active_index >= len(self._parent.children) - 1,
                tooltip="次の兄弟ノードへ",
            )

            header_controls.extend([
                left_btn,
                ft.Text(
                    f"{self._parent.active_index + 1}/{len(self._parent.children)}",
                    style=ft.TextThemeStyle.BODY_SMALL,
                ),
                right_btn,
            ])

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

        # Body content based on mode
        if self._edit_mode:
            # Edit mode - show TextFields and action buttons
            update_btn = ft.FilledButton(
                text="更新",
                on_click=self._update_and_exit,
            )
            cancel_btn = ft.OutlinedButton(
                text="キャンセル",
                on_click=self._cancel_and_exit,
            )
            buttons = ft.Row([update_btn, cancel_btn], spacing=8)

            body = ft.Column([
                header,
                self.reasoning_tf,
                self.response_tf,
                buttons
            ], spacing=8)
        else:
            # Display mode - show compact Text widgets
            body_controls: List[ft.Control] = [header]

            if self._node.reasoning:
                body_controls.append(ft.ExpansionTile(
                    title=ft.Text(value="Reasoning", style=ft.TextThemeStyle.LABEL_MEDIUM),
                    controls=[ft.Text(
                        value=self._node.reasoning,
                        style=ft.TextThemeStyle.BODY_MEDIUM,
                        selectable=True,
                    )]))

            if self._node.response:
                body_controls.append(ft.Text(
                    value=self._node.response,
                    style=ft.TextThemeStyle.BODY_MEDIUM,
                    selectable=True,
                ))

            body = ft.Column(body_controls, spacing=4)

        return body

    def _enter_edit_mode(self, _):
        """Enter edit mode and save original values for potential cancellation."""
        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, _):
        """Update the node and exit edit mode."""
        self._svc.update_text(
            self._node,
            self.reasoning_tf.value,
            self.response_tf.value
        )
        self._edit_mode = False

    def _cancel_and_exit(self, _):
        """Cancel editing and exit edit mode without saving changes."""
        self._edit_mode = False
        # Restore original values (no actual changes to node)
        self.reasoning_tf.value = self._original_reasoning
        self.response_tf.value = self._original_response
        self._rebuild()

    def _rebuild(self):
        """Rebuild the card content."""
        self.content = self._build_content()
        self.update()


# ────────────────────────── 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 = [a, b, c]
    root.active_index = 1  # 'b' is selected

    b1 = TreeNode(role="assistant", reasoning="[東京への行き方を考える]", response="すみません、よくわかりませんでした。")
    b2 = TreeNode(role="assistant", reasoning="[東京への行き方を考える]", response="好きに行けばいいと思います。")
    b.children = [b1, b2]

    return Tree(root_nodes=[root])


# ──────────────────────────────── Flet main ───────────────────────────────────
def main(page: ft.Page) -> None:
    page.title = "TreeNode Active Path (display/edit modes)"
    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(
                ChatCard(node=node, parent_node=parent, service=service)
            )
        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)
```
Flet 0.28.3.
カードの縁にカード内のコンテンツが接してしまっている。一箇所の設定でいい感じに周囲に隙間を作るには、どこで設定をするべき？