diff --git a/pygittools/tui/app.py b/pygittools/tui/app.py
index ba941de..b9a4c75 100644
--- a/pygittools/tui/app.py
+++ b/pygittools/tui/app.py
@@ -4,6 +4,13 @@ from __future__ import annotations
 
 from pathlib import Path
 
+from pygittools.tui.git_colors import init_tui_color_theme, status_bar_attr
+from pygittools.tui.pages.home import HomePage, home_page
+from pygittools.tui.pages.no_repo import NoRepoPage
+from pygittools.tui.repo import try_open_page_context
+from pygittools.tui.status_bar import content_height, draw_status_bar
+from pygittools.tui.tabs import init_tab_colors
+from pygittools.tui.types import Page, PageAction, PageContext, PageResult
 from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	KEY_RESIZE,
 	cbreak,
@@ -18,14 +25,6 @@ from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	refresh,
 )
 
-from pygittools.tui.git_colors import init_tui_color_theme, status_bar_attr
-from pygittools.tui.pages.home import HomePage, home_page
-from pygittools.tui.pages.no_repo import NoRepoPage
-from pygittools.tui.repo import try_open_page_context
-from pygittools.tui.status_bar import content_height, draw_status_bar
-from pygittools.tui.tabs import init_tab_colors
-from pygittools.tui.types import Page, PageAction, PageContext, PageResult
-
 
 def run_tui(repo_path: Path | None = None) -> int:
 	start = (repo_path or Path.cwd()).resolve()
diff --git a/pygittools/tui/changes_list.py b/pygittools/tui/changes_list.py
index c0d7882..cf91956 100644
--- a/pygittools/tui/changes_list.py
+++ b/pygittools/tui/changes_list.py
@@ -8,6 +8,10 @@ from pathlib import Path
 from typing import Literal
 
 from pygit2 import Repository
+
+from pygittools.tui.commit_draft import save_commit_draft
+from pygittools.tui.draw import clear_row, draw_line, draw_text
+from pygittools.tui.git_colors import current_theme, focus_attr, row_attr_for_section
 from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	A_BOLD,
 	KEY_BACKSPACE,
@@ -18,10 +22,6 @@ from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	KEY_RIGHT,
 	KEY_UP,
 )
-
-from pygittools.tui.commit_draft import save_commit_draft
-from pygittools.tui.draw import clear_row, draw_line, draw_text
-from pygittools.tui.git_colors import current_theme, focus_attr, row_attr_for_section
 from pygittools.tui.worktree import (
 	categorize_status,
 	commit_staged,
diff --git a/pygittools/tui/changes_list_test.py b/pygittools/tui/changes_list_test.py
index 2037ee8..ab09e4c 100644
--- a/pygittools/tui/changes_list_test.py
+++ b/pygittools/tui/changes_list_test.py
@@ -4,10 +4,10 @@ from pathlib import Path
 
 import pytest
 from pygit2 import Repository
-from pygittools.tui.ucurses import KEY_BACKSPACE, KEY_DOWN, KEY_ENTER, KEY_RESIZE, KEY_UP
 
 from pygittools.tui.changes_list import ChangesList, SectionId
 from pygittools.tui.types import PageContext
+from pygittools.tui.ucurses import KEY_BACKSPACE, KEY_DOWN, KEY_ENTER, KEY_RESIZE, KEY_UP
 from pygittools.tui.worktree import categorize_status
 
 
diff --git a/pygittools/tui/coverage_gaps_test.py b/pygittools/tui/coverage_gaps_test.py
index 85d0a19..8cc56e1 100644
--- a/pygittools/tui/coverage_gaps_test.py
+++ b/pygittools/tui/coverage_gaps_test.py
@@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch
 
 import pytest
 from pygit2 import Repository, init_repository
-from pygittools.tui.ucurses import KEY_BACKSPACE, KEY_ENTER, KEY_RESIZE, KEY_RIGHT, KEY_UP
 
 from pygittools.tui.app import _apply_result, run_tui
 from pygittools.tui.changes_list import ChangesList, SectionId
@@ -23,6 +22,7 @@ from pygittools.tui.pygitweb_layout import _bool_value, _int_value, _layout_from
 from pygittools.tui.status_bar import _format_line
 from pygittools.tui.tabs import _clip_prefix, tab_row_layout
 from pygittools.tui.types import PageAction, PageContext, PageResult
+from pygittools.tui.ucurses import KEY_BACKSPACE, KEY_ENTER, KEY_RESIZE, KEY_RIGHT, KEY_UP
 from pygittools.tui.worktree import commit_staged, unstage_paths
 
 
diff --git a/pygittools/tui/git_colors.py b/pygittools/tui/git_colors.py
index 5e48d77..1f054e7 100644
--- a/pygittools/tui/git_colors.py
+++ b/pygittools/tui/git_colors.py
@@ -9,6 +9,7 @@ from importlib.resources import files
 from typing import TYPE_CHECKING
 
 from pygit2 import Config
+
 from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	A_BOLD,
 	A_DIM,
diff --git a/pygittools/tui/input_context.py b/pygittools/tui/input_context.py
index e47efb3..eafa00f 100644
--- a/pygittools/tui/input_context.py
+++ b/pygittools/tui/input_context.py
@@ -3,8 +3,14 @@
 from __future__ import annotations
 
 from dataclasses import dataclass
-from typing import Literal, Protocol
+from typing import TYPE_CHECKING, Literal, Protocol
 
+from pygittools.tui.changes_list import ChangesList
+from pygittools.tui.log_list import CommitLogList
+from pygittools.tui.pages.branches import branch_picker_page
+from pygittools.tui.pages.help import help_page
+from pygittools.tui.pages.projects import projects_page
+from pygittools.tui.types import PageAction, PageContext, PageResult
 from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	KEY_DOWN,
 	KEY_ENTER,
@@ -13,16 +19,12 @@ from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	KEY_UP,
 )
 
-from pygittools.tui.changes_list import ChangesList
-from pygittools.tui.log_list import CommitLogList
-from pygittools.tui.pages.branches import branch_picker_page
-from pygittools.tui.pages.help import help_page
-from pygittools.tui.pages.projects import projects_page
-from pygittools.tui.types import PageAction, PageContext, PageResult
+if TYPE_CHECKING:
+	from pygittools.tui.tasks_list import TasksBoardList
 
 _KEY_ESC = 27
 _HeaderFocus = Literal["changes", "repo", "branch"]
-_HomeTab = Literal["status", "log"]
+_HomeTab = Literal["status", "log", "tasks"]
 
 
 @dataclass(frozen=True, slots=True)
@@ -50,6 +52,8 @@ class PageInputHost(Protocol):
 	def active_tab(self) -> _HomeTab: ...
 
 	def set_active_tab(self, tab: _HomeTab) -> None: ...
+	@property
+	def tasks(self) -> TasksBoardList: ...
 
 
 class NavContext:
@@ -60,6 +64,8 @@ class NavContext:
 			return "Enter switch branch — j/k move — H/L switch tab"
 		if host.active_tab == "log":
 			return host.log.status_hint()
+		if host.active_tab == "tasks":
+			return host.tasks.status_hint()
 		return host.changes.status_hint()
 
 	def dispatch_key(self, host: PageInputHost, key: int) -> ContextDispatch:
@@ -95,6 +101,9 @@ class NavContext:
 		if host.active_tab == "log":
 			host.log.handle_nav_key(key, ctx.repo)
 			return ContextDispatch()
+		if host.active_tab == "tasks":
+			page_result = host.tasks.handle_nav_key(key, ctx.repo)
+			return ContextDispatch(page_result=page_result)
 		nav_result = host.changes.handle_nav_key(key, ctx.repo)
 		if nav_result.enter_input:
 			return ContextDispatch(next_mode="input")
@@ -131,16 +140,21 @@ def input_context() -> InputContext:
 def _content_list_is_at_top(host: PageInputHost) -> bool:
 	if host.active_tab == "log":
 		return host.log.is_at_top()
+	if host.active_tab == "tasks":
+		return host.tasks.is_at_top()
 	return host.changes.is_at_top()
 
 
 def _try_switch_tab(host: PageInputHost, key: int) -> bool:
+	tabs: tuple[_HomeTab, ...] = ("status", "log", "tasks")
+	current = host.active_tab
+	index = tabs.index(current)
 	if key in (KEY_LEFT, ord("h"), ord("H")):
-		if host.active_tab == "log":
-			host.set_active_tab("status")
+		if index > 0:
+			host.set_active_tab(tabs[index - 1])
 		return True
 	if key in (KEY_RIGHT, ord("l"), ord("L")):
-		if host.active_tab == "status":
-			host.set_active_tab("log")
+		if index < len(tabs) - 1:
+			host.set_active_tab(tabs[index + 1])
 		return True
 	return False
diff --git a/pygittools/tui/input_context_test.py b/pygittools/tui/input_context_test.py
index aaeaf22..7ff70a0 100644
--- a/pygittools/tui/input_context_test.py
+++ b/pygittools/tui/input_context_test.py
@@ -4,13 +4,13 @@ from dataclasses import dataclass
 from typing import Literal
 
 import pytest
-from pygittools.tui.ucurses import KEY_DOWN, KEY_ENTER, KEY_LEFT, KEY_RIGHT, KEY_UP
 
 from pygittools.tui.changes_list import ChangesList
 from pygittools.tui.input_context import InputContext, input_context, nav_context
 from pygittools.tui.log_list import CommitLogList
 from pygittools.tui.pages.help import HelpPage
 from pygittools.tui.types import PageAction, PageContext
+from pygittools.tui.ucurses import KEY_DOWN, KEY_ENTER, KEY_LEFT, KEY_RIGHT, KEY_UP
 
 _HeaderFocus = Literal["changes", "repo", "branch"]
 _HomeTab = Literal["status", "log"]
diff --git a/pygittools/tui/log_list.py b/pygittools/tui/log_list.py
index 75635a1..acd3dfc 100644
--- a/pygittools/tui/log_list.py
+++ b/pygittools/tui/log_list.py
@@ -6,15 +6,15 @@ from dataclasses import dataclass
 from datetime import UTC, datetime
 
 from pygit2 import GIT_SORT_TIME, Commit, GitError, Repository
+
+from pygittools.tui.draw import clear_row, draw_text
+from pygittools.tui.git_colors import current_theme, log_entry_attrs
 from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	KEY_DOWN,
 	KEY_RESIZE,
 	KEY_UP,
 )
 
-from pygittools.tui.draw import clear_row, draw_text
-from pygittools.tui.git_colors import current_theme, log_entry_attrs
-
 _MAX_COMMITS = 500
 
 
diff --git a/pygittools/tui/pages/branches.py b/pygittools/tui/pages/branches.py
index 4b12ad7..804fc9a 100644
--- a/pygittools/tui/pages/branches.py
+++ b/pygittools/tui/pages/branches.py
@@ -2,6 +2,14 @@
 
 from __future__ import annotations
 
+from pygittools.tui.branches import (
+	checkout_branch,
+	create_branch_from_current,
+	current_branch_name,
+	list_local_branches,
+)
+from pygittools.tui.draw import draw_line
+from pygittools.tui.types import PageAction, PageContext, PageResult
 from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	A_BOLD,
 	A_REVERSE,
@@ -12,15 +20,6 @@ from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	KEY_UP,
 )
 
-from pygittools.tui.branches import (
-	checkout_branch,
-	create_branch_from_current,
-	current_branch_name,
-	list_local_branches,
-)
-from pygittools.tui.draw import draw_line
-from pygittools.tui.types import PageAction, PageContext, PageResult
-
 _FILTER_PREFIX = "Filter: "
 
 
diff --git a/pygittools/tui/pages/help.py b/pygittools/tui/pages/help.py
index 4c712e4..74fbf0f 100644
--- a/pygittools/tui/pages/help.py
+++ b/pygittools/tui/pages/help.py
@@ -4,15 +4,14 @@ from __future__ import annotations
 
 from importlib.resources import files
 
+from pygittools.tui.draw import draw_line
+from pygittools.tui.types import PageAction, PageContext, PageResult
 from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	KEY_DOWN,
 	KEY_RESIZE,
 	KEY_UP,
 )
 
-from pygittools.tui.draw import draw_line
-from pygittools.tui.types import PageAction, PageContext, PageResult
-
 
 def _load_help_lines() -> list[str]:
 	text = files("pygittools").joinpath("help.md").read_text(encoding="utf-8")
diff --git a/pygittools/tui/pages/home.py b/pygittools/tui/pages/home.py
index 7b32b5b..d1018dd 100644
--- a/pygittools/tui/pages/home.py
+++ b/pygittools/tui/pages/home.py
@@ -4,10 +4,6 @@ from __future__ import annotations
 
 from typing import Literal
 
-from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
-	KEY_RESIZE,
-)
-
 from pygittools.tui.branches import current_branch_name
 from pygittools.tui.changes_list import ChangesList
 from pygittools.tui.commit_draft import load_commit_draft
@@ -17,14 +13,19 @@ from pygittools.tui.input_context import InputContext, InputMode, input_context,
 from pygittools.tui.log_list import CommitLogList
 from pygittools.tui.project_label import repo_project_label
 from pygittools.tui.tabs import draw_tab_row
+from pygittools.tui.tasks_list import TasksBoardList
 from pygittools.tui.types import Page, PageAction, PageContext, PageResult
+from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
+	KEY_RESIZE,
+)
 
 _PROJECT_ROW = 0
 _BRANCH_ROW = 1
 _HEADER_ROWS = 2
-_TABS: tuple[str, ...] = ("Status", "Log")
+_TABS: tuple[str, ...] = ("Status", "Log", "Tasks")
 _HeaderFocus = Literal["changes", "repo", "branch"]
-_HomeTab = Literal["status", "log"]
+_HomeTab = Literal["status", "log", "tasks"]
+_TAB_KEYS: tuple[_HomeTab, ...] = ("status", "log", "tasks")
 
 
 class HomePage:
@@ -34,6 +35,7 @@ class HomePage:
 		self._ctx: PageContext | None = None
 		self._changes = ChangesList()
 		self._log = CommitLogList()
+		self._tasks = TasksBoardList()
 		self._active_tab: _HomeTab = "status"
 		self._header_focus: _HeaderFocus = "changes"
 		self._input_mode: InputMode = nav_context()
@@ -58,11 +60,17 @@ class HomePage:
 		return self._log
 
 	@property
+	def tasks(self) -> TasksBoardList:
+		return self._tasks
+
+	@property
 	def active_tab(self) -> _HomeTab:
 		return self._active_tab
 
 	def set_active_tab(self, tab: _HomeTab) -> None:
 		self._active_tab = tab
+		if tab != "status" and isinstance(self._input_mode, InputContext):
+			self._input_mode = nav_context()
 
 	def on_enter(self, ctx: PageContext) -> None:
 		self._ctx = ctx
@@ -72,6 +80,7 @@ class HomePage:
 		self._changes.set_commit_message(load_commit_draft(ctx.repo_path, ctx.repo))
 		self._changes.refresh(ctx.repo)
 		self._log.refresh(ctx.repo)
+		self._tasks.refresh(ctx.repo)
 
 	def status_text(self) -> str:
 		return self._input_mode.status_hint(self)
@@ -84,11 +93,15 @@ class HomePage:
 
 		self._changes.refresh(ctx.repo)
 		self._log.refresh(ctx.repo)
+		self._tasks.refresh(ctx.repo)
 		project_label = repo_project_label(ctx.repo_path)
 		branch = current_branch_name(ctx.repo)
 
 		if height > _PROJECT_ROW:
-			active_index = 0 if self._active_tab == "status" else 1
+			try:
+				active_index = _TAB_KEYS.index(self._active_tab)
+			except ValueError:
+				active_index = 0
 			theme = current_theme()
 			prefix_attr = focus_attr(theme.branch) if self._header_focus == "repo" else 0
 			draw_tab_row(_PROJECT_ROW, width, project_label, _TABS, active_index, prefix_attr=prefix_attr)
@@ -111,8 +124,10 @@ class HomePage:
 					highlight=content_highlight,
 					commit_input=commit_input,
 				)
-			else:
+			elif self._active_tab == "log":
 				self._log.draw(_HEADER_ROWS, list_height, width, highlight=content_highlight)
+			else:
+				self._tasks.draw(_HEADER_ROWS, list_height, width, highlight=content_highlight)
 
 	def handle_key(self, key: int) -> PageResult:
 		if key in (ord("q"), ord("Q")):
diff --git a/pygittools/tui/pages/no_repo.py b/pygittools/tui/pages/no_repo.py
index 0a669ba..84ee7c5 100644
--- a/pygittools/tui/pages/no_repo.py
+++ b/pygittools/tui/pages/no_repo.py
@@ -4,11 +4,10 @@ from __future__ import annotations
 
 from pathlib import Path
 
-from pygittools.tui.ucurses import A_BOLD, KEY_RESIZE  # type: ignore[import-untyped]
-
 from pygittools.tui.draw import draw_line
 from pygittools.tui.pages.help import help_page
 from pygittools.tui.types import PageAction, PageContext, PageResult
+from pygittools.tui.ucurses import A_BOLD, KEY_RESIZE  # type: ignore[import-untyped]
 
 
 class NoRepoPage:
diff --git a/pygittools/tui/pages/projects.py b/pygittools/tui/pages/projects.py
index 31fa480..d21ded9 100644
--- a/pygittools/tui/pages/projects.py
+++ b/pygittools/tui/pages/projects.py
@@ -2,6 +2,10 @@
 
 from __future__ import annotations
 
+from pygittools.tui.draw import draw_line
+from pygittools.tui.projects import ProjectEntry, list_projects
+from pygittools.tui.pygitweb_layout import load_pygitweb_layout
+from pygittools.tui.types import PageAction, PageContext, PageResult
 from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	A_BOLD,
 	A_REVERSE,
@@ -11,11 +15,6 @@ from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
 	KEY_UP,
 )
 
-from pygittools.tui.draw import draw_line
-from pygittools.tui.projects import ProjectEntry, list_projects
-from pygittools.tui.pygitweb_layout import load_pygitweb_layout
-from pygittools.tui.types import PageAction, PageContext, PageResult
-
 _BRANCH_WIDTH = 16
 _TABLE_TOP = 2
 
diff --git a/pygittools/tui/pages/task_editor.py b/pygittools/tui/pages/task_editor.py
new file mode 100644
index 0000000..f1c3309
--- /dev/null
+++ b/pygittools/tui/pages/task_editor.py
@@ -0,0 +1,321 @@
+"""Task editor page for the pgt TUI."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from datetime import datetime
+from typing import Literal
+
+from pygit2 import Oid
+
+from pygittools.tasks import BOARD_REF_PREFIX, EMPTY_TREE_OID_HEX, TASK_REF_PREFIX, Task, get_board, get_task
+from pygittools.tasks_query import BOARD_STATUS_ORDER, get_task_view
+from pygittools.tui.draw import draw_line
+from pygittools.tui.git_colors import current_theme, focus_attr
+from pygittools.tui.types import PageAction, PageContext, PageResult
+from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
+	KEY_BACKSPACE,
+	KEY_DOWN,
+	KEY_ENTER,
+	KEY_RESIZE,
+	KEY_UP,
+)
+
+_FieldName = Literal["title", "description", "status", "priority", "assignee", "due_date"]
+_FIELDS: tuple[_FieldName, ...] = ("title", "description", "status", "priority", "assignee", "due_date")
+_PRIORITY_ORDER: tuple[str, ...] = ("LOW", "MEDIUM", "HIGH", "CRITICAL")
+
+
+@dataclass(frozen=True, slots=True)
+class _EditorState:
+	title: str
+	description: str
+	status: str | None
+	priority: str | None
+	assignee: str
+	due_date: str
+
+
+class TaskEditorPage:
+	title = "task"
+
+	def __init__(self, *, board_name: str, task_ref: str | None, default_status: str | None) -> None:
+		self._ctx: PageContext | None = None
+		self._board_name = board_name
+		self._task_ref = task_ref
+		self._default_status = default_status
+		self._state = _EditorState(
+			title="",
+			description="",
+			status=default_status,
+			priority=None,
+			assignee="",
+			due_date="",
+		)
+		self._field_index = 0
+		self._input_mode: Literal["nav", "input"] = "input"
+		self._error: str | None = None
+
+	def on_enter(self, ctx: PageContext) -> None:
+		self._ctx = ctx
+		if self._task_ref is None:
+			return
+		view = get_task_view(ctx.repo, self._task_ref)
+		self._state = _EditorState(
+			title=view.get("title", ""),
+			description=view.get("description", ""),
+			status=view.get("status"),
+			priority=view.get("priority"),
+			assignee=view.get("assignee") or "",
+			due_date=view.get("due_date") or "",
+		)
+
+	def status_text(self) -> str:
+		if self._error:
+			return self._error
+		return "Enter save — j/k move field — type to edit — Esc or q back"
+
+	def draw(self, stdscr: int, height: int, width: int) -> None:
+		del stdscr
+		if height <= 0:
+			return
+		theme = current_theme()
+		header = f"Task in board {self._board_name}"
+		draw_line(0, 0, header[:width], width, theme.header)
+		if height <= 1:
+			return
+
+		rows = [
+			f"Title: {self._state.title}",
+			f"Description: {self._state.description}",
+			f"Status: {self._state.status or '(none)'}",
+			f"Priority: {self._state.priority or '(none)'}",
+			f"Assignee: {self._state.assignee}",
+			f"Due date (ISO): {self._state.due_date}",
+		]
+		for index, text in enumerate(rows):
+			if index + 1 >= height:
+				break
+			attr = theme.normal
+			if index == self._field_index:
+				attr = focus_attr(attr)
+			draw_line(index + 1, 0, text[:width], width, attr)
+
+	def handle_key(self, key: int) -> PageResult:
+		if key in (ord("q"), ord("Q")):
+			return PageResult(action=PageAction.POP)
+		if key == 27:
+			if self._input_mode == "input":
+				self._input_mode = "nav"
+				return PageResult()
+			return PageResult(action=PageAction.POP)
+		if key == KEY_RESIZE:
+			return PageResult()
+		if self._error is not None:
+			self._error = None
+
+		field = _FIELDS[self._field_index]
+
+		# Input mode: edit text fields, cycle status/priority; j/k/h/l are text in text fields.
+		if self._input_mode == "input":
+			if field in ("title", "description", "assignee", "due_date"):
+				if key in (KEY_UP, KEY_DOWN):
+					# Move between fields with arrows even while in input mode.
+					if key == KEY_UP:
+						self._field_index = max(0, self._field_index - 1)
+					else:
+						self._field_index = min(len(_FIELDS) - 1, self._field_index + 1)
+					return PageResult()
+				if key in (KEY_ENTER, 10, 13):
+					return self._save()
+				return self._edit_text_field(field, key)
+			if field == "status":
+				self._cycle_status(key)
+				return PageResult()
+			if field == "priority":
+				self._cycle_priority(key)
+				return PageResult()
+			return PageResult()
+
+		# Nav mode: j/k and arrows move between fields; Enter saves.
+		if key in (KEY_UP, ord("k"), ord("K")):
+			self._field_index = max(0, self._field_index - 1)
+			return PageResult()
+		if key in (KEY_DOWN, ord("j"), ord("J")):
+			self._field_index = min(len(_FIELDS) - 1, self._field_index + 1)
+			return PageResult()
+		if key in (KEY_ENTER, 10, 13):
+			return self._save()
+		return PageResult()
+
+	def _edit_text_field(self, field: _FieldName, key: int) -> PageResult:
+		if key in (KEY_BACKSPACE, 127, 8):
+			self._state = _replace_state_field(self._state, field, _text_for_field(self._state, field)[:-1])
+			return PageResult()
+		if 32 <= key <= 126:
+			text = _text_for_field(self._state, field) + chr(key)
+			self._state = _replace_state_field(self._state, field, text)
+			return PageResult()
+		return PageResult()
+
+	def _cycle_status(self, key: int) -> None:
+		if key not in (KEY_UP, KEY_DOWN, ord("j"), ord("J"), ord("k"), ord("K")):
+			return
+		current = self._state.status or self._default_status or BOARD_STATUS_ORDER[0]
+		try:
+			index = BOARD_STATUS_ORDER.index(current)
+		except ValueError:
+			index = 0
+		if key in (KEY_UP, ord("k"), ord("K")):
+			index = (index - 1) % len(BOARD_STATUS_ORDER)
+		else:
+			index = (index + 1) % len(BOARD_STATUS_ORDER)
+		self._state = _EditorState(
+			title=self._state.title,
+			description=self._state.description,
+			status=BOARD_STATUS_ORDER[index],
+			priority=self._state.priority,
+			assignee=self._state.assignee,
+			due_date=self._state.due_date,
+		)
+
+	def _cycle_priority(self, key: int) -> None:
+		if key not in (KEY_UP, KEY_DOWN, ord("j"), ord("J"), ord("k"), ord("K")):
+			return
+		if not _PRIORITY_ORDER:
+			return
+		current = self._state.priority or _PRIORITY_ORDER[0]
+		try:
+			index = _PRIORITY_ORDER.index(current)
+		except ValueError:
+			index = 0
+		if key in (KEY_UP, ord("k"), ord("K")):
+			index = (index - 1) % len(_PRIORITY_ORDER)
+		else:
+			index = (index + 1) % len(_PRIORITY_ORDER)
+		self._state = _EditorState(
+			title=self._state.title,
+			description=self._state.description,
+			status=self._state.status,
+			priority=_PRIORITY_ORDER[index],
+			assignee=self._state.assignee,
+			due_date=self._state.due_date,
+		)
+
+	def _save(self) -> PageResult:
+		ctx = self._ctx
+		if ctx is None:
+			return PageResult(action=PageAction.POP)
+
+		try:
+			due_dt = _parse_due_date(self._state.due_date)
+		except ValueError as exc:
+			self._error = str(exc)
+			return PageResult()
+
+		if self._task_ref is None:
+			try:
+				_create_task(ctx, self._board_name, self._state, due_dt)
+			except Exception as exc:  # noqa: BLE001
+				self._error = str(exc)
+				return PageResult()
+			return PageResult(action=PageAction.POP)
+
+		task = get_task(ctx.repo, self._task_ref)
+		if task is None:
+			self._error = f"Task not found: {self._task_ref}"
+			return PageResult()
+		task.title = self._state.title
+		task.description = self._state.description
+		task.status = Task.Status(self._state.status) if self._state.status else None
+		task.priority = Task.Priority(self._state.priority) if self._state.priority else None
+		task.assignee = self._state.assignee or None
+		task.due_date = due_dt
+		task.update_message()
+		task.write(ctx.repo)
+		return PageResult(action=PageAction.POP)
+
+
+def _replace_state_field(state: _EditorState, field: _FieldName, value: str) -> _EditorState:
+	if field == "title":
+		return _EditorState(
+			title=value,
+			description=state.description,
+			status=state.status,
+			priority=state.priority,
+			assignee=state.assignee,
+			due_date=state.due_date,
+		)
+	if field == "description":
+		return _EditorState(
+			title=state.title,
+			description=value,
+			status=state.status,
+			priority=state.priority,
+			assignee=state.assignee,
+			due_date=state.due_date,
+		)
+	if field == "assignee":
+		return _EditorState(
+			title=state.title,
+			description=state.description,
+			status=state.status,
+			priority=state.priority,
+			assignee=value,
+			due_date=state.due_date,
+		)
+	return _EditorState(
+		title=state.title,
+		description=state.description,
+		status=state.status,
+		priority=state.priority,
+		assignee=state.assignee,
+		due_date=value,
+	)
+
+
+def _text_for_field(state: _EditorState, field: _FieldName) -> str:
+	if field == "title":
+		return state.title
+	if field == "description":
+		return state.description
+	if field == "assignee":
+		return state.assignee
+	return state.due_date
+
+
+def _parse_due_date(text: str) -> datetime | None:
+	if not text.strip():
+		return None
+	try:
+		return datetime.fromisoformat(text)
+	except ValueError as exc:
+		raise ValueError(f"Invalid due date (expected ISO format): {text}") from exc
+
+
+def _create_task(ctx: PageContext, board_name: str, state: _EditorState, due_dt: datetime | None) -> None:
+	board_ref = f"{BOARD_REF_PREFIX}{board_name}"
+	board = get_board(ctx.repo, board_ref)
+	if board is None:
+		raise ValueError(f"Board not found: {board_name}")
+	title = state.title or "Untitled"
+	slug = title.lower().replace(" ", "_") or "task"
+	full_ref = f"{TASK_REF_PREFIX}{slug}"
+	target = Oid(hex=EMPTY_TREE_OID_HEX)
+	sig = ctx.repo.default_signature
+	tagger = f"{sig.name} <{sig.email}>"
+	task = Task(
+		target,
+		full_ref,
+		tagger,
+		title=title,
+		description=state.description,
+		status=Task.Status(state.status) if state.status else None,
+		priority=Task.Priority(state.priority) if state.priority else None,
+		assignee=state.assignee or None,
+		due_date=due_dt,
+	)
+	task_oid = task.write(ctx.repo)
+	board.tasks = [*(getattr(board, "tasks", []) or []), str(task_oid)]
+	board.update_message()
+	board.write(ctx.repo)
diff --git a/pygittools/tui/pages_test.py b/pygittools/tui/pages_test.py
index 3422604..4b04dfe 100644
--- a/pygittools/tui/pages_test.py
+++ b/pygittools/tui/pages_test.py
@@ -5,15 +5,17 @@ from pathlib import Path
 from unittest.mock import MagicMock
 
 import pytest
-from pygit2 import Repository, Signature
-from pygittools.tui.ucurses import KEY_BACKSPACE, KEY_DOWN, KEY_ENTER, KEY_RESIZE, KEY_UP
+from pygit2 import Oid, Repository, Signature
 
+from pygittools.tasks import BOARD_REF_PREFIX, EMPTY_TREE_OID_HEX, TASK_REF_PREFIX, Board, Task
 from pygittools.tui.pages.branches import BranchPickerPage, branch_picker_page
 from pygittools.tui.pages.help import HelpPage, help_page
 from pygittools.tui.pages.home import home_page
 from pygittools.tui.pages.no_repo import NoRepoPage
 from pygittools.tui.pages.projects import ProjectsPage, projects_page
+from pygittools.tui.pages.task_editor import TaskEditorPage
 from pygittools.tui.types import PageAction, PageContext
+from pygittools.tui.ucurses import KEY_BACKSPACE, KEY_DOWN, KEY_ENTER, KEY_RESIZE, KEY_UP
 
 
 def test_home_page_on_enter_and_quit(page_ctx: PageContext) -> None:
@@ -31,6 +33,8 @@ def test_home_page_draw_and_tabs(committed_repo: Repository) -> None:
 	page.draw(0, 20, 80)
 	page.set_active_tab("log")
 	page.draw(0, 20, 80)
+	page.set_active_tab("tasks")
+	page.draw(0, 20, 80)
 	page.set_header_focus("repo")
 	page.draw(0, 20, 80)
 	page.handle_key(KEY_RESIZE)
@@ -162,3 +166,48 @@ def test_projects_page_empty(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) ->
 
 def test_projects_page_draw_zero_height() -> None:
 	ProjectsPage().draw(0, 0, 80)
+
+
+def _empty_tree() -> Oid:
+	return Oid(hex=EMPTY_TREE_OID_HEX)
+
+
+def test_home_page_tasks_tab_creates_and_edits_tasks(committed_repo: Repository) -> None:
+	board_ref = f"{BOARD_REF_PREFIX}Tasks"
+	board = Board(_empty_tree(), board_ref, tagger="", description="Main")
+	task = Task(
+		_empty_tree(),
+		f"{TASK_REF_PREFIX}alpha",
+		"alice <alice@example.com>",
+		title="Alpha",
+		description="Do alpha",
+		status=Task.Status.TODO,
+	)
+	task_oid = task.write(committed_repo)
+	board.tasks = [str(task_oid)]
+	board.update_message()
+	board.write(committed_repo)
+
+	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
+	page = home_page()
+	page.on_enter(ctx)
+	page.set_active_tab("tasks")
+	page.draw(0, 20, 80)
+
+	# Move to first status header and then to (+ New Task)
+	page.handle_key(KEY_DOWN)
+	page.handle_key(KEY_DOWN)
+	result_new = page.handle_key(KEY_ENTER)
+	assert result_new.action == PageAction.PUSH
+	assert isinstance(result_new.next_page, TaskEditorPage)
+
+	# Move to existing task row and open editor
+	page = home_page()
+	page.on_enter(ctx)
+	page.set_active_tab("tasks")
+	page.handle_key(KEY_DOWN)
+	page.handle_key(KEY_DOWN)
+	page.handle_key(KEY_DOWN)
+	result_existing = page.handle_key(KEY_ENTER)
+	assert result_existing.action == PageAction.PUSH
+	assert isinstance(result_existing.next_page, TaskEditorPage)
diff --git a/pygittools/tui/tasks_list.py b/pygittools/tui/tasks_list.py
new file mode 100644
index 0000000..c6bac12
--- /dev/null
+++ b/pygittools/tui/tasks_list.py
@@ -0,0 +1,259 @@
+"""Collapsible task board view for the pgt TUI."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Literal
+
+from pygit2 import Repository
+
+from pygittools.tasks_query import BOARD_STATUS_ORDER, BoardView, get_board_tasks_grouped, list_boards
+from pygittools.tui.draw import draw_line
+from pygittools.tui.git_colors import current_theme, focus_attr
+from pygittools.tui.types import PageAction, PageResult
+from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
+	KEY_DOWN,
+	KEY_ENTER,
+	KEY_LEFT,
+	KEY_RESIZE,
+	KEY_RIGHT,
+	KEY_UP,
+)
+
+
+@dataclass(frozen=True, slots=True)
+class _Row:
+	kind: Literal["board", "status_header", "new_task", "task"]
+	status: str | None
+	task_ref: str | None
+	label: str
+
+
+class TasksBoardList:
+	def __init__(self) -> None:
+		self._boards: list[BoardView] = []
+		self._board_index = 0
+		self._board_name: str | None = None
+		self._collapsed: dict[str, bool] = {status: False for status in BOARD_STATUS_ORDER}
+		self._rows: list[_Row] = []
+		self._cursor = 0
+		self._scroll_offset = 0
+		self._error: str | None = None
+
+	@property
+	def board_name(self) -> str | None:
+		return self._board_name
+
+	def is_at_top(self) -> bool:
+		return self._cursor == 0
+
+	def refresh(self, repo: Repository) -> None:
+		self._boards = list_boards(repo)
+		if not self._boards:
+			self._rows = []
+			self._board_name = None
+			self._cursor = 0
+			self._scroll_offset = 0
+			return
+
+		if self._board_name is None:
+			self._board_index = _default_board_index(self._boards)
+		else:
+			self._board_index = _board_index_for_name(self._boards, self._board_name)
+		self._board_index = max(0, min(self._board_index, len(self._boards) - 1))
+		self._board_name = self._boards[self._board_index]["name"]
+
+		rows: list[_Row] = []
+		board = self._boards[self._board_index]
+		board_label = f"Board: {board['name']} ({board['task_count']} tasks)"
+		rows.append(_Row(kind="board", status=None, task_ref=None, label=board_label))
+
+		try:
+			columns = get_board_tasks_grouped(repo, self._board_name or "")
+		except KeyError as exc:
+			self._error = str(exc)
+			self._rows = rows
+			self._cursor = min(self._cursor, max(0, len(self._rows) - 1))
+			self._scroll_offset = 0
+			return
+
+		for column in columns:
+			status = column["status"]
+			label = column["label"]
+			tasks = column["tasks"]
+			if status not in self._collapsed:
+				self._collapsed[status] = False
+			marker = ">" if self._collapsed[status] else "v"
+			header = f"[{marker}] {label} ({len(tasks)})"
+			rows.append(_Row(kind="status_header", status=status, task_ref=None, label=header))
+			if self._collapsed[status]:
+				continue
+			rows.append(
+				_Row(
+					kind="new_task",
+					status=status,
+					task_ref=None,
+					label="(+ New Task)",
+				),
+			)
+			for task in tasks:
+				title = task.get("title") or "(untitled)"
+				rows.append(
+					_Row(
+						kind="task",
+						status=status,
+						task_ref=task.get("ref"),
+						label=f"  {title}",
+					),
+				)
+
+		self._rows = rows
+		self._cursor = min(self._cursor, max(0, len(self._rows) - 1))
+		self._ensure_cursor_visible(10)
+
+	def draw(self, top_row: int, height: int, width: int, *, highlight: bool) -> None:
+		if height <= 0:
+			return
+		self._ensure_cursor_visible(height)
+
+		if not self._rows and not self._error:
+			draw_line(
+				top_row, 0, "No task boards — use pygittools tasks to create one", width, current_theme().log_date
+			)
+			return
+
+		if self._error:
+			draw_line(top_row, 0, self._error[:width], width, current_theme().log_date)
+
+		for view_row in range(height):
+			row_index = self._scroll_offset + view_row
+			if row_index >= len(self._rows):
+				break
+			row = self._rows[row_index]
+			focused = highlight and row_index == self._cursor
+			screen_row = top_row + view_row
+			attr = _row_attr(row, focused=focused, highlight=highlight)
+			draw_line(screen_row, 0, row.label, width, attr)
+
+	def handle_nav_key(self, key: int, repo: Repository) -> PageResult | None:
+		if key == KEY_RESIZE:
+			return None
+		if self._error is not None:
+			self._error = None
+		if not self._rows and not self._boards:
+			self.refresh(repo)
+			return None
+
+		if key in (KEY_UP, ord("k"), ord("K")):
+			self._cursor = max(0, self._cursor - 1)
+			self._ensure_cursor_visible(10)
+			return None
+		if key in (KEY_DOWN, ord("j"), ord("J")):
+			self._cursor = min(max(0, len(self._rows) - 1), self._cursor + 1)
+			self._ensure_cursor_visible(10)
+			return None
+		if not self._rows:
+			return None
+
+		row = self._rows[self._cursor]
+
+		if row.kind == "board" and self._boards:
+			if key in (KEY_LEFT, ord("h"), ord("H")) and self._board_index > 0:
+				self._board_index -= 1
+				self._board_name = self._boards[self._board_index]["name"]
+				self._cursor = 0
+				self._scroll_offset = 0
+				self.refresh(repo)
+				return None
+			if key in (KEY_RIGHT, ord("l"), ord("L")) and self._board_index < len(self._boards) - 1:
+				self._board_index += 1
+				self._board_name = self._boards[self._board_index]["name"]
+				self._cursor = 0
+				self._scroll_offset = 0
+				self.refresh(repo)
+				return None
+			return None
+
+		if row.kind == "status_header" and row.status is not None:
+			if key in (KEY_LEFT, KEY_RIGHT, ord(" ")):
+				self._collapsed[row.status] = not self._collapsed.get(row.status, False)
+				self.refresh(repo)
+				return None
+			if key in (KEY_ENTER, 10, 13):
+				self._collapsed[row.status] = not self._collapsed.get(row.status, False)
+				self.refresh(repo)
+				return None
+			return None
+
+		if key in (KEY_ENTER, 10, 13):
+			if row.kind == "new_task" and row.status is not None and self._board_name is not None:
+				return PageResult(
+					action=PageAction.PUSH,
+					next_page=_task_editor_page(self._board_name, None, default_status=row.status),
+				)
+			if row.kind == "task" and row.task_ref is not None and self._board_name is not None:
+				return PageResult(
+					action=PageAction.PUSH,
+					next_page=_task_editor_page(self._board_name, row.task_ref, default_status=row.status),
+				)
+		return None
+
+	def status_hint(self) -> str:
+		if self._error:
+			return self._error
+		if not self._boards:
+			return "No task boards — use pygittools tasks to create one"
+		if not self._rows:
+			return "No tasks — use (+ New Task) to create one"
+		row = self._rows[self._cursor]
+		if row.kind == "board":
+			return "H/L switch board — j/k move"
+		if row.kind == "status_header":
+			return "Enter collapse — Space collapse — j/k move"
+		if row.kind == "new_task":
+			return "Enter create task — j/k move"
+		return "Enter edit task — j/k move"
+
+	def _ensure_cursor_visible(self, height: int) -> None:
+		if height <= 0:
+			return
+		if self._cursor < self._scroll_offset:
+			self._scroll_offset = self._cursor
+		elif self._cursor >= self._scroll_offset + height:
+			self._scroll_offset = self._cursor - height + 1
+
+
+def _default_board_index(boards: list[BoardView]) -> int:
+	for index, board in enumerate(boards):
+		if board["name"].casefold() == "tasks":
+			return index
+	return 0
+
+
+def _board_index_for_name(boards: list[BoardView], name: str | None) -> int:
+	if name is None:
+		return _default_board_index(boards)
+	for index, board in enumerate(boards):
+		if board["name"] == name:
+			return index
+	return _default_board_index(boards)
+
+
+def _row_attr(row: _Row, *, focused: bool, highlight: bool) -> int:
+	theme = current_theme()
+	if row.kind == "board":
+		base = theme.header
+	elif row.kind == "status_header":
+		base = theme.section_header
+	else:
+		base = theme.normal
+	if focused and highlight:
+		return focus_attr(base)
+	return base
+
+
+def _task_editor_page(board_name: str, task_ref: str | None, *, default_status: str | None):
+	# Lazy import to avoid circular dependency.
+	from pygittools.tui.pages.task_editor import TaskEditorPage
+
+	return TaskEditorPage(board_name=board_name, task_ref=task_ref, default_status=default_status)
