code-context-control 2.44.1__py3-none-any.whl → 2.45.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cli/c3.py CHANGED
@@ -85,7 +85,7 @@ console = Console() if HAS_RICH else None
85
85
  # Config
86
86
  CONFIG_DIR = ".c3"
87
87
  CONFIG_FILE = ".c3/config.json"
88
- __version__ = "2.44.1"
88
+ __version__ = "2.45.0"
89
89
 
90
90
 
91
91
  def _command_deps() -> CommandDeps:
@@ -239,7 +239,7 @@ _C3_MCP_ALLOW = [
239
239
  "mcp__c3__c3_memory", "mcp__c3__c3_validate", "mcp__c3__c3_edit",
240
240
  "mcp__c3__c3_agent", "mcp__c3__c3_delegate", "mcp__c3__c3_edits",
241
241
  "mcp__c3__c3_impact", "mcp__c3__c3_shell", "mcp__c3__c3_bitbucket",
242
- "mcp__c3__c3_project",
242
+ "mcp__c3__c3_project", "mcp__c3__c3_task",
243
243
  ]
244
244
 
245
245
  # Obsolete MCP tool names from earlier C3 versions. `c3 permissions clean`
cli/guide/tools.html CHANGED
@@ -121,6 +121,7 @@
121
121
  </div>
122
122
  <div class="sidebar-section">
123
123
  <div class="sidebar-label">SCM</div>
124
+ <a href="#c3_task" class="sidebar-link"><span class="icon">✅</span> c3_task</a>
124
125
  <a href="#c3_bitbucket" class="sidebar-link"><span class="icon">🪣</span> c3_bitbucket</a>
125
126
  </div>
126
127
  <div class="sidebar-section">
@@ -155,6 +156,7 @@
155
156
  <a href="#c3_delegate" class="toc-item">c3_delegate <span class="cat">AI</span></a>
156
157
  <a href="#c3_agent" class="toc-item">c3_agent <span class="cat">AI</span></a>
157
158
  <a href="#c3_edits" class="toc-item">c3_edits <span class="cat">Ledger</span></a>
159
+ <a href="#c3_task" class="toc-item">c3_task <span class="cat">PM</span></a>
158
160
  <a href="#c3_bitbucket" class="toc-item">c3_bitbucket <span class="cat">SCM</span></a>
159
161
  <a href="#c3_project" class="toc-item">c3_project <span class="cat">Multi-project</span></a>
160
162
  </div>
@@ -1090,6 +1092,52 @@ c3_edits(action=<span class="str">'history'</span>, branch=<span class="str">'fe
1090
1092
  </div>
1091
1093
  </div>
1092
1094
 
1095
+ <!-- c3_task -->
1096
+ <div class="tool-card" id="c3_task">
1097
+ <div class="tool-card-header">
1098
+ <span class="tool-name">c3_task</span>
1099
+ <div class="tag-row">
1100
+ <span class="badge badge-purple">pm</span>
1101
+ <span class="badge">v2.45.0</span>
1102
+ </div>
1103
+ <span class="tool-tagline">Durable per-project tasks, milestones, and decision notes — the project-management layer</span>
1104
+ </div>
1105
+ <div class="tool-card-body">
1106
+ <p class="tool-desc">Every C3 project carries a PM store at <code>.c3/pm/pm.json</code>: tasks with status (backlog / in_progress / blocked / done), priority (p0–p3), due dates, tags, and code links (files, commits, edit-ledger entries); milestones with computed progress; and a decision-note log separate from AI memory. The same store powers the Hub's Tasks tab and kanban board, the per-project UI's Tasks tab, and this tool. Task ids accept any unique prefix (≥4 chars); milestones resolve by id or unique name. Read actions are plan-mode-safe. Ephemeral session plans stay in <code>c3_session(action='plan')</code>.</p>
1107
+
1108
+ <h4>Actions</h4>
1109
+ <table class="params-table">
1110
+ <thead><tr><th>Action</th><th>Group</th><th>Description</th></tr></thead>
1111
+ <tbody>
1112
+ <tr><td class="param-name">add</td><td>Write</td><td class="param-desc">Create a task: <code>title</code> + optional description / priority / due_date (YYYY-MM-DD) / tags (CSV) / milestone</td></tr>
1113
+ <tr><td class="param-name">update, done, archive</td><td>Write</td><td class="param-desc"><code>task_id</code> + changed fields; <code>done</code> stamps completion; archive keeps history</td></tr>
1114
+ <tr><td class="param-name">list, get, board</td><td>Read</td><td class="param-desc">Filtered list (status/priority/tags/milestone/query), full detail, kanban columns + milestone progress</td></tr>
1115
+ <tr><td class="param-name">link, unlink</td><td>Write</td><td class="param-desc"><code>task_id</code> + <code>link_type</code> (file | commit | edit) + <code>ref</code> — tie tasks to the code they touch</td></tr>
1116
+ <tr><td class="param-name">milestone_add / _update / _list / _archive</td><td>Both</td><td class="param-desc">Milestones with target dates; list shows progress %; archive detaches its tasks</td></tr>
1117
+ <tr><td class="param-name">note_add, note_list</td><td>Both</td><td class="param-desc">Dated notes; <code>kind='decision'</code> for the decision log</td></tr>
1118
+ </tbody>
1119
+ </table>
1120
+
1121
+ <h4>Examples</h4>
1122
+ <pre><code><span class="com"># Capture a task while coding</span>
1123
+ c3_task(action=<span class="str">'add'</span>, title=<span class="str">'Harden pm.json against concurrent writes'</span>,
1124
+ priority=<span class="str">'p1'</span>, tags=<span class="str">'pm,backend'</span>)
1125
+
1126
+ <span class="com"># Tie it to the code it touches, then finish it</span>
1127
+ c3_task(action=<span class="str">'link'</span>, task_id=<span class="str">'a1b2c3d4'</span>, link_type=<span class="str">'file'</span>, ref=<span class="str">'services/task_store.py'</span>)
1128
+ c3_task(action=<span class="str">'done'</span>, task_id=<span class="str">'a1b2'</span>)
1129
+
1130
+ <span class="com"># Milestones + the board</span>
1131
+ c3_task(action=<span class="str">'milestone_add'</span>, name=<span class="str">'v2.46'</span>, target_date=<span class="str">'2026-08-01'</span>)
1132
+ c3_task(action=<span class="str">'board'</span>)
1133
+
1134
+ <span class="com"># Record a decision</span>
1135
+ c3_task(action=<span class="str">'note_add'</span>, note=<span class="str">'Kanban rank uses float sort keys'</span>, kind=<span class="str">'decision'</span>)</code></pre>
1136
+
1137
+ <p class="tool-desc"><strong>Surfaces:</strong> the Hub shows a Tasks tab per project (drill-in panel), <code>☑ N</code> open-task chips on cards, and a global kanban board behind the Projects | Tasks switcher. Hub mutations audit <code>pm_write</code> events to the project's activity log. Disable with <code>hybrid.pm.enabled=false</code>.</p>
1138
+ </div>
1139
+ </div>
1140
+
1093
1141
  <!-- c3_bitbucket -->
1094
1142
  <div class="tool-card" id="c3_bitbucket">
1095
1143
  <div class="tool-card-header">
cli/hub_server.py CHANGED
@@ -67,6 +67,7 @@ _HUB_CONFIG_DEFAULTS = {
67
67
  "auto_open_browser": True,
68
68
  "theme": "dark",
69
69
  "projects_view": "list",
70
+ "main_view": "projects",
70
71
  "oracle_url": "",
71
72
  }
72
73
 
@@ -319,6 +320,7 @@ _HUB_JS_FILES = [
319
320
  "ui/icons.js",
320
321
  "ui/api.js",
321
322
  "ui/shared.js",
323
+ "ui/pm_shared.js",
322
324
  "hub_ui/state.js",
323
325
  "hub_ui/components/toasts.js",
324
326
  "hub_ui/components/topbar.js",
@@ -327,10 +329,12 @@ _HUB_JS_FILES = [
327
329
  "hub_ui/components/summary_bar.js",
328
330
  "hub_ui/components/project_card.js",
329
331
  "hub_ui/components/project_tree.js",
332
+ "hub_ui/components/task_board.js",
330
333
  "hub_ui/components/session_drawer.js",
331
334
  "hub_ui/components/drill_panel.js",
332
335
  "hub_ui/components/drill_views.js",
333
336
  "hub_ui/components/drill_health.js",
337
+ "hub_ui/components/drill_tasks.js",
334
338
  "hub_ui/components/config_editor.js",
335
339
  "hub_ui/components/mcp_manager.js",
336
340
  "hub_ui/components/global_search.js",
@@ -429,6 +433,11 @@ def api_hub_config_set():
429
433
  if projects_view not in {"list", "grid"}:
430
434
  return jsonify({"error": "projects_view must be 'list' or 'grid'"}), 400
431
435
  cfg["projects_view"] = projects_view
436
+ if "main_view" in data:
437
+ main_view = str(data["main_view"]).strip().lower()
438
+ if main_view not in {"projects", "board"}:
439
+ return jsonify({"error": "main_view must be 'projects' or 'board'"}), 400
440
+ cfg["main_view"] = main_view
432
441
  if "oracle_url" in data:
433
442
  cfg["oracle_url"] = str(data["oracle_url"]).strip()
434
443
  if "sidebar_group" in data:
@@ -470,11 +479,13 @@ def _notification_count(project_path: str) -> int:
470
479
  @app.route("/api/projects", methods=["GET"])
471
480
  def api_projects_list():
472
481
  try:
482
+ from services.task_store import open_task_count
473
483
  projects = _pm().list_projects()
474
484
  parent_paths = {os.path.normcase(p["parent_path"]) for p in projects if p.get("parent_path")}
475
485
  for p in projects:
476
486
  p["notification_count"] = _notification_count(p.get("path", ""))
477
487
  p["is_parent"] = os.path.normcase(p.get("path", "")) in parent_paths
488
+ p["open_task_count"] = open_task_count(p.get("path", ""))
478
489
  return jsonify(projects)
479
490
  except Exception as e:
480
491
  return jsonify({"error": str(e)}), 500
@@ -1463,11 +1474,13 @@ def api_projects_inspect():
1463
1474
  ledger_total = int(rt.edit_ledger.get_stats().get("total", 0))
1464
1475
  except Exception:
1465
1476
  ledger_total = 0
1477
+ task_store = getattr(rt, "task_store", None)
1466
1478
  counts = {
1467
1479
  "facts": len(rt.memory.facts),
1468
1480
  "edits": ledger_total,
1469
1481
  "sessions": len(rt.session_mgr.list_sessions(500)),
1470
1482
  "notifications": _notification_count(resolved["path"]),
1483
+ "tasks_open": task_store.stats()["open"] if task_store else 0,
1471
1484
  }
1472
1485
  return jsonify({"project": details, "counts": counts})
1473
1486
  if view == "memory":
@@ -1484,6 +1497,13 @@ def api_projects_inspect():
1484
1497
  })
1485
1498
  if view == "sessions":
1486
1499
  return jsonify({"sessions": rt.session_mgr.list_sessions(limit)})
1500
+ if view == "tasks":
1501
+ task_store = getattr(rt, "task_store", None)
1502
+ if task_store is None:
1503
+ return jsonify({"board": {"columns": {}, "milestones": [], "stats": {}},
1504
+ "notes": []})
1505
+ return jsonify({"board": task_store.board(),
1506
+ "notes": task_store.list_notes(limit=20)})
1487
1507
  return jsonify({"error": f"unknown view '{view}'"}), 400
1488
1508
  except Exception as e:
1489
1509
  return jsonify({"error": str(e)}), 500
@@ -1659,6 +1679,268 @@ def api_projects_config_put():
1659
1679
  return jsonify({"saved": True, "section": section, "config": merged})
1660
1680
 
1661
1681
 
1682
+ # ── Project management: tasks / milestones / notes (v2.45.0) ──────────────
1683
+ # Direct TaskStore per request (reload-per-op store — no runtime build needed
1684
+ # for mutations); every write audited to the target project's activity log.
1685
+
1686
+ def _pm_resolve(path: str):
1687
+ """Resolve a PM request path -> (Path, error_response|None)."""
1688
+ if not path:
1689
+ return None, (jsonify({"error": "path is required"}), 400)
1690
+ try:
1691
+ resolved = _resolve_project_path(path)
1692
+ except ValueError as e:
1693
+ return None, (jsonify({"error": str(e)}), 404)
1694
+ if not (resolved / ".c3").is_dir():
1695
+ return None, (jsonify({"error": "not initialized", "needs_init": True}), 409)
1696
+ return resolved, None
1697
+
1698
+
1699
+ def _pm_store(path):
1700
+ from services.task_store import TaskStore
1701
+ return TaskStore(str(path))
1702
+
1703
+
1704
+ def _pm_audit(path, entity, op, item_id=""):
1705
+ try:
1706
+ ActivityLog(str(path)).log("pm_write", {
1707
+ "entity": entity, "op": op, "id": item_id, "source": "hub"})
1708
+ except Exception:
1709
+ pass
1710
+
1711
+
1712
+ @app.route("/api/projects/pm", methods=["GET"])
1713
+ def api_projects_pm_get():
1714
+ """Full PM board for one project. Query: path, milestone?, tag?,
1715
+ include_children? (parent rollup), include_archived?"""
1716
+ resolved, err = _pm_resolve((request.args.get("path") or "").strip())
1717
+ if err:
1718
+ return err
1719
+ store = _pm_store(resolved)
1720
+ board = store.board(
1721
+ milestone_id=(request.args.get("milestone") or None),
1722
+ tag=(request.args.get("tag") or None),
1723
+ include_archived=request.args.get("include_archived") == "1",
1724
+ )
1725
+ out = {"path": str(resolved), "board": board, "notes": store.list_notes(limit=100)}
1726
+ if request.args.get("include_children") == "1":
1727
+ children = []
1728
+ try:
1729
+ from services.subprojects import SubprojectManager
1730
+ for c in SubprojectManager(str(resolved)).list():
1731
+ if c["status"] in ("missing_folder", "missing_c3"):
1732
+ continue
1733
+ rows = [t for t in _pm_store(c["path"]).list_tasks(limit=100)
1734
+ if t.get("status") != "done"]
1735
+ children.append({"name": c["name"], "path": c["path"], "tasks": rows})
1736
+ except Exception:
1737
+ pass
1738
+ out["children"] = children
1739
+ return jsonify(out)
1740
+
1741
+
1742
+ @app.route("/api/projects/pm/task", methods=["POST", "PUT", "DELETE"])
1743
+ def api_pm_task():
1744
+ data = request.get_json(force=True) or {}
1745
+ resolved, err = _pm_resolve((data.get("path") or "").strip())
1746
+ if err:
1747
+ return err
1748
+ store = _pm_store(resolved)
1749
+
1750
+ if request.method == "POST":
1751
+ res = store.create_task(
1752
+ data.get("title", ""), description=data.get("description", ""),
1753
+ status=data.get("status") or "backlog",
1754
+ priority=data.get("priority") or "p2",
1755
+ due_date=data.get("due_date") or None,
1756
+ tags=data.get("tags") or [], milestone_id=data.get("milestone_id"),
1757
+ links=data.get("links") or [], created_by="hub")
1758
+ if "error" in res:
1759
+ return jsonify(res), 400
1760
+ _pm_audit(resolved, "task", "create", res["id"])
1761
+ return jsonify({"created": True, "task": res}), 201
1762
+
1763
+ if request.method == "PUT":
1764
+ task_id = (data.get("id") or "").strip()
1765
+ if not task_id:
1766
+ return jsonify({"error": "id is required"}), 400
1767
+ res = None
1768
+ fields = data.get("fields") or {}
1769
+ if fields:
1770
+ res = store.update_task(task_id, **fields)
1771
+ if "error" in res:
1772
+ return jsonify(res), 400
1773
+ move = data.get("move") or {}
1774
+ if move:
1775
+ res = store.move_task(task_id, status=move.get("status"),
1776
+ before_id=move.get("before_id"),
1777
+ after_id=move.get("after_id"))
1778
+ if "error" in res:
1779
+ return jsonify(res), 400
1780
+ if res is None:
1781
+ return jsonify({"error": "fields or move required"}), 400
1782
+ _pm_audit(resolved, "task", "update", res["id"])
1783
+ return jsonify({"updated": True, "task": res})
1784
+
1785
+ # DELETE: archive by id, or purge all archived with {purge: true}
1786
+ if data.get("purge"):
1787
+ res = store.purge_archived("task")
1788
+ _pm_audit(resolved, "task", "purge")
1789
+ return jsonify(res)
1790
+ task_id = (data.get("id") or "").strip()
1791
+ if not task_id:
1792
+ return jsonify({"error": "id is required"}), 400
1793
+ res = store.archive_task(task_id)
1794
+ if "error" in res:
1795
+ return jsonify(res), 400
1796
+ _pm_audit(resolved, "task", "archive", res["id"])
1797
+ return jsonify({"archived": True, "task": res})
1798
+
1799
+
1800
+ @app.route("/api/projects/pm/milestone", methods=["POST", "PUT", "DELETE"])
1801
+ def api_pm_milestone():
1802
+ data = request.get_json(force=True) or {}
1803
+ resolved, err = _pm_resolve((data.get("path") or "").strip())
1804
+ if err:
1805
+ return err
1806
+ store = _pm_store(resolved)
1807
+
1808
+ if request.method == "POST":
1809
+ res = store.create_milestone(data.get("name", ""),
1810
+ description=data.get("description", ""),
1811
+ target_date=data.get("target_date") or None)
1812
+ if "error" in res:
1813
+ return jsonify(res), 400
1814
+ _pm_audit(resolved, "milestone", "create", res["id"])
1815
+ return jsonify({"created": True, "milestone": res}), 201
1816
+
1817
+ ms_id = (data.get("id") or "").strip()
1818
+ if not ms_id:
1819
+ return jsonify({"error": "id is required"}), 400
1820
+
1821
+ if request.method == "PUT":
1822
+ res = store.update_milestone(ms_id, **(data.get("fields") or {}))
1823
+ if "error" in res:
1824
+ return jsonify(res), 400
1825
+ _pm_audit(resolved, "milestone", "update", res["id"])
1826
+ return jsonify({"updated": True, "milestone": res})
1827
+
1828
+ res = store.archive_milestone(ms_id) # DELETE = archive + detach tasks
1829
+ if "error" in res:
1830
+ return jsonify(res), 400
1831
+ _pm_audit(resolved, "milestone", "archive", res["id"])
1832
+ return jsonify({"archived": True, "milestone": res})
1833
+
1834
+
1835
+ @app.route("/api/projects/pm/note", methods=["POST", "PUT", "DELETE"])
1836
+ def api_pm_note():
1837
+ data = request.get_json(force=True) or {}
1838
+ resolved, err = _pm_resolve((data.get("path") or "").strip())
1839
+ if err:
1840
+ return err
1841
+ store = _pm_store(resolved)
1842
+
1843
+ if request.method == "POST":
1844
+ res = store.add_note(data.get("text", ""), kind=data.get("kind") or "note",
1845
+ tags=data.get("tags") or [],
1846
+ task_id=data.get("task_id"), author="hub")
1847
+ if "error" in res:
1848
+ return jsonify(res), 400
1849
+ _pm_audit(resolved, "note", "create", res["id"])
1850
+ return jsonify({"created": True, "note": res}), 201
1851
+
1852
+ note_id = (data.get("id") or "").strip()
1853
+ if not note_id:
1854
+ return jsonify({"error": "id is required"}), 400
1855
+
1856
+ if request.method == "PUT":
1857
+ res = store.update_note(note_id, **(data.get("fields") or {}))
1858
+ if "error" in res:
1859
+ return jsonify(res), 400
1860
+ _pm_audit(resolved, "note", "update", res["id"])
1861
+ return jsonify({"updated": True, "note": res})
1862
+
1863
+ res = store.archive_note(note_id)
1864
+ if "error" in res:
1865
+ return jsonify(res), 400
1866
+ _pm_audit(resolved, "note", "archive", res["id"])
1867
+ return jsonify({"archived": True, "note": res})
1868
+
1869
+
1870
+ @app.route("/api/projects/pm/link", methods=["POST"])
1871
+ def api_pm_link():
1872
+ data = request.get_json(force=True) or {}
1873
+ resolved, err = _pm_resolve((data.get("path") or "").strip())
1874
+ if err:
1875
+ return err
1876
+ task_id = (data.get("id") or "").strip()
1877
+ link = data.get("link") or {}
1878
+ op = (data.get("op") or "add").strip()
1879
+ if not task_id or not link.get("type") or not link.get("ref"):
1880
+ return jsonify({"error": "id and link {type, ref} are required"}), 400
1881
+ if op not in ("add", "remove"):
1882
+ return jsonify({"error": "op must be add|remove"}), 400
1883
+ store = _pm_store(resolved)
1884
+ if op == "add":
1885
+ res = store.add_link(task_id, link["type"], link["ref"],
1886
+ label=link.get("label", ""))
1887
+ else:
1888
+ res = store.remove_link(task_id, link["type"], link["ref"])
1889
+ if "error" in res:
1890
+ return jsonify(res), 400
1891
+ _pm_audit(resolved, "link", op, res["id"])
1892
+ return jsonify({"task": res})
1893
+
1894
+
1895
+ @app.route("/api/pm/global", methods=["GET"])
1896
+ def api_pm_global():
1897
+ """Open tasks across every registered project (raw registry — no port probes)."""
1898
+ try:
1899
+ limit = max(1, min(int(request.args.get("limit") or 500), 1000))
1900
+ except ValueError:
1901
+ limit = 500
1902
+ status_filter = (request.args.get("status") or "").strip()
1903
+ from services.task_store import TaskStore
1904
+
1905
+ tasks, skipped, by_project = [], [], {}
1906
+ entries = _pm()._read_projects()
1907
+ for p in entries:
1908
+ ppath = p.get("path") or ""
1909
+ if not Path(ppath).is_dir():
1910
+ skipped.append({"path": ppath, "reason": "not accessible"})
1911
+ continue
1912
+ if not (Path(ppath) / ".c3").is_dir():
1913
+ skipped.append({"path": ppath, "reason": "not initialized"})
1914
+ continue
1915
+ try:
1916
+ if status_filter and status_filter != "all":
1917
+ rows = TaskStore(ppath).list_tasks(status=status_filter, limit=1000)
1918
+ else:
1919
+ rows = TaskStore(ppath).list_tasks(limit=1000)
1920
+ if status_filter != "all":
1921
+ rows = [t for t in rows if t.get("status") != "done"]
1922
+ except Exception as e:
1923
+ skipped.append({"path": ppath, "reason": str(e)})
1924
+ continue
1925
+ if not rows:
1926
+ continue
1927
+ proj_info = {"name": p.get("name"), "path": ppath}
1928
+ if p.get("parent_path"):
1929
+ proj_info["parent_path"] = p["parent_path"]
1930
+ for t in rows:
1931
+ t["project"] = proj_info
1932
+ tasks.extend(rows)
1933
+ by_project[ppath] = {"name": p.get("name"), "open": len(rows)}
1934
+
1935
+ # priority asc, due asc, updated desc (stable two-stage sort)
1936
+ tasks.sort(key=lambda t: t.get("updated_at") or "", reverse=True)
1937
+ tasks.sort(key=lambda t: (t.get("priority", "p2"), t.get("due_date") or "9999"))
1938
+ capped = len(tasks) > limit
1939
+ return jsonify({"projects_scanned": len(entries) - len(skipped),
1940
+ "skipped": skipped, "capped": capped,
1941
+ "tasks": tasks[:limit], "by_project": by_project})
1942
+
1943
+
1662
1944
  @app.route("/api/projects/run-mcp", methods=["POST"])
1663
1945
  def api_run_mcp():
1664
1946
  data = request.get_json(force=True) or {}
cli/hub_ui/app.js CHANGED
@@ -17,6 +17,7 @@ function App() {
17
17
  const [version, setVersion] = useState('');
18
18
  const [projects, setProjects] = useState([]);
19
19
  const [loaded, setLoaded] = useState(false);
20
+ const [mainView, setMainView] = useState('projects'); // projects | board
20
21
  const [filter, setFilter] = useState('all'); // all | active | idle | tag:<x>
21
22
  const [search, setSearch] = useState('');
22
23
  const [view, setView] = useState('list'); // list | grid
@@ -43,6 +44,7 @@ function App() {
43
44
  if (cfg.projects_view === 'grid') setView('grid');
44
45
  if (cfg.sidebar_collapsed != null) setSidebarCollapsed(!!cfg.sidebar_collapsed);
45
46
  if (cfg.sidebar_group) setFilter(cfg.sidebar_group);
47
+ if (cfg.main_view === 'board') setMainView('board');
46
48
  } catch { }
47
49
  try { const v = await api.get('/api/version'); setVersion(v.c3_version || ''); } catch { }
48
50
  }, []);
@@ -68,6 +70,10 @@ function App() {
68
70
  setSidebarCollapsed(c);
69
71
  api.post('/api/hub/config', { sidebar_collapsed: c }).catch(() => { });
70
72
  };
73
+ const changeMainView = (v) => {
74
+ setMainView(v);
75
+ api.post('/api/hub/config', { main_view: v }).catch(() => { });
76
+ };
71
77
 
72
78
  // Keyboard: Ctrl/Cmd-K opens cross-project search, Esc closes overlays
73
79
  useEffect(() => {
@@ -92,7 +98,7 @@ function App() {
92
98
  return (
93
99
  <div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: T.bg, color: T.text }}>
94
100
  <TopBar version={version} activeCount={activeCount} darkMode={darkMode}
95
- hubConfig={hubConfig}
101
+ hubConfig={hubConfig} mainView={mainView} setMainView={changeMainView}
96
102
  onToggleTheme={toggleTheme}
97
103
  onOpenSettings={() => openModal('settings')}
98
104
  onOpenSearch={() => setSearchOpen(true)}
@@ -104,13 +110,19 @@ function App() {
104
110
  flex: 1, minWidth: 0, overflowY: 'auto', padding: '18px 22px',
105
111
  display: 'flex', flexDirection: 'column', gap: 12,
106
112
  }}>
107
- <SummaryBar projects={projects} search={search} setSearch={setSearch}
108
- view={view} setView={changeView} filter={filter} setFilter={changeFilter}
109
- onUpdateAll={() => openModal('batch')}
110
- onAddProject={() => openModal('add')} />
111
- <ProjectTree projects={visible} allProjects={projects} view={view} loaded={loaded}
112
- onChanged={loadProjects} onOpenDrill={openDrill} onOpenModal={openModal}
113
- onOpenDrawer={setDrawerProject} />
113
+ {mainView === 'board' ? (
114
+ <TaskBoard projects={projects} onOpenDrill={openDrill} />
115
+ ) : (
116
+ <React.Fragment>
117
+ <SummaryBar projects={projects} search={search} setSearch={setSearch}
118
+ view={view} setView={changeView} filter={filter} setFilter={changeFilter}
119
+ onUpdateAll={() => openModal('batch')}
120
+ onAddProject={() => openModal('add')} />
121
+ <ProjectTree projects={visible} allProjects={projects} view={view} loaded={loaded}
122
+ onChanged={loadProjects} onOpenDrill={openDrill} onOpenModal={openModal}
123
+ onOpenDrawer={setDrawerProject} />
124
+ </React.Fragment>
125
+ )}
114
126
  </main>
115
127
  </div>
116
128
  {drawerProject &&
@@ -5,6 +5,7 @@
5
5
 
6
6
  const DRILL_PANEL_TABS = [
7
7
  ['overview', 'Overview'],
8
+ ['tasks', 'Tasks'],
8
9
  ['memory', 'Memory'],
9
10
  ['ledger', 'Ledger'],
10
11
  ['sessions', 'Sessions'],
@@ -107,6 +108,7 @@ function DrillNeedsInit({ project, onReady }) {
107
108
  function DrillPanel({ project, tab, setTab, onClose, onChanged, onOpenModal }) {
108
109
  const renderTab = () => {
109
110
  switch (tab) {
111
+ case 'tasks': return <DrillTasks project={project} onChanged={onChanged} />;
110
112
  case 'memory': return <DrillMemory project={project} />;
111
113
  case 'ledger': return <DrillLedger project={project} />;
112
114
  case 'sessions': return <DrillSessions project={project} />;