From 4e9a48e3b70af3191ac2af31a87036d04278d055 Mon Sep 17 00:00:00 2001 From: Mohammad Ausaf Date: Sat, 27 Jun 2026 23:23:53 +0530 Subject: [PATCH 1/2] feat: add UI recent activity rail --- README.md | 14 +++++++----- docs/USAGE.md | 5 ++++- skills/cfgit/SKILL.md | 2 ++ src/cfg/core/engine.py | 5 +++++ src/cfg/interfaces/actions.py | 6 +++++ src/cfg/mcp/server.py | 17 ++++++++++++++ src/cfg/ui/server.py | 42 ++++++++++++++++++++++++++++++++++- tests/test_engine_safety.py | 22 ++++++++++++++++++ tests/test_mcp_identity.py | 19 ++++++++++++++++ tests/test_ui_server.py | 9 ++++++++ 10 files changed, 134 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 58eee7b..ca64938 100644 --- a/README.md +++ b/README.md @@ -337,11 +337,14 @@ Refs: `cfg ui` starts a localhost-only web UI over the same action layer as the CLI and MCP server. It reads like a git client: a collection-and-record tree on the left, -a commit-graph history rail, and a line-aligned side-by-side diff that collapses -unchanged context (expandable in place) and keeps the field name pinned while you -scroll. It can run status, diff, impact, commit, branch draft commits, PR open -and merge, log, show, adopt, restore, tag, init, import, and fsck, and ships -dark and light themes. +a recent-activity rail before you select anything, per-record commit graphs, and +a line-aligned side-by-side diff that collapses unchanged context (expandable in +place) and keeps the field name pinned while you scroll. The recent activity view +surfaces live drift and the latest cfgit commits across all configured records, +so you can see what changed recently without opening records one by one. It can +run status, diff, impact, commit, branch draft commits, PR open and merge, log, +show, adopt, restore, tag, init, import, and fsck, and ships dark and light +themes. By default it binds to `127.0.0.1:8765` and tries the next free ports if needed: @@ -385,6 +388,7 @@ Tools include: - `cfg_pr_show` - `cfg_pr_close` - `cfg_pr_merge` +- `cfg_recent_history` - `cfg_log` - `cfg_show` - `cfg_adopt` diff --git a/docs/USAGE.md b/docs/USAGE.md index 6d505eb..50e32f5 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -269,7 +269,10 @@ cfg ui ``` The UI binds to `127.0.0.1`. It is a local operator surface over the same action -layer as the CLI and MCP server. +layer as the CLI and MCP server. Before a record is selected, the middle rail +shows recent activity across all configured records: current live drift plus the +latest cfgit history entries. Selecting one of those entries opens that record's +normal history and diff view. ## JSON mode diff --git a/skills/cfgit/SKILL.md b/skills/cfgit/SKILL.md index 3a2e336..29deb7f 100644 --- a/skills/cfgit/SKILL.md +++ b/skills/cfgit/SKILL.md @@ -33,6 +33,7 @@ Use cfgit as the safety layer around a live datastore. The app still reads and w 2. Inspect before mutation. - `cfg diff =HEAD =live --json` - `cfg log --json` + - Use MCP `cfg_recent_history` when you need the latest cfgit commits across all configured records before choosing a record. - `cfg impact =HEAD =live --json` - Scoped LLM review, when explicitly needed: `cfg impact =HEAD =live --against --llm --json` - If history lookup reports an env mismatch, re-run against the stamped env before making changes. @@ -83,6 +84,7 @@ If the cfgit MCP server is available, prefer its tools over shelling out: - `cfg_pr_show` - `cfg_pr_close` - `cfg_pr_merge` +- `cfg_recent_history` - `cfg_log` - `cfg_show` - `cfg_adopt` diff --git a/src/cfg/core/engine.py b/src/cfg/core/engine.py index c69b690..5027e8c 100644 --- a/src/cfg/core/engine.py +++ b/src/cfg/core/engine.py @@ -1102,6 +1102,11 @@ def log(self, ref: RecordRef, *, limit: int | None = None) -> list[dict[str, Any with_doc=False, ) + def recent_history(self, *, limit: int | None = 50) -> list[dict[str, Any]]: + rows = self.adapter.query_history(limit=None, order="desc", with_doc=False) + rows = sorted(rows, key=lambda row: (row.get("recorded_at"), row["collection"], row["record_id"], row["seq"]), reverse=True) + return rows[:limit] if limit is not None else rows + def tag(self, name: str) -> list[dict[str, Any]]: self._authorize("tag") tagged: list[dict[str, Any]] = [] diff --git a/src/cfg/interfaces/actions.py b/src/cfg/interfaces/actions.py index 28b2c9d..44320a2 100644 --- a/src/cfg/interfaces/actions.py +++ b/src/cfg/interfaces/actions.py @@ -175,6 +175,10 @@ def log(engine: Engine, record: str, *, limit: int | None = 20) -> tuple[list[di return engine.log(parse_record(record), limit=limit), EXIT_OK +def recent_history(engine: Engine, *, limit: int | None = 50) -> tuple[list[dict[str, Any]], int]: + return engine.recent_history(limit=limit), EXIT_OK + + def show(engine: Engine, record: str, ref: str) -> tuple[dict[str, Any], int]: return engine.resolve_ref(parse_record(record), ref), EXIT_OK @@ -373,6 +377,8 @@ def run_named_action(name: str, engine: Engine, payload: dict[str, Any] | None = ) if name == "log": return log(engine, _required(payload, "record"), limit=int(payload.get("limit") or 20)) + if name == "recent_history": + return recent_history(engine, limit=int(payload.get("limit") or 50)) if name == "show": return show(engine, _required(payload, "record"), str(payload.get("ref") or "HEAD")) if name == "adopt": diff --git a/src/cfg/mcp/server.py b/src/cfg/mcp/server.py index eb09d64..27008b1 100644 --- a/src/cfg/mcp/server.py +++ b/src/cfg/mcp/server.py @@ -197,6 +197,23 @@ def cfg_log( ) +@mcp.tool() +def cfg_recent_history( + limit: int = 50, + config_file: str | None = None, + env: str = "dev", + author: str | None = None, +) -> dict[str, Any]: + """Return recent cfgit history entries across all configured records.""" + return _call( + "recent_history", + {"limit": limit}, + config_file=config_file, + env=env, + author=author, + ) + + @mcp.tool() def cfg_show( record: str, diff --git a/src/cfg/ui/server.py b/src/cfg/ui/server.py index 9648691..15d2038 100644 --- a/src/cfg/ui/server.py +++ b/src/cfg/ui/server.py @@ -125,6 +125,7 @@ def _state(self, params: dict[str, list[str]]) -> dict[str, Any]: except Exception: branches = [] prs = [] + recent, _ = actions.recent_history(engine, limit=50) return { "status": "dirty" if code == actions.EXIT_DIRTY else "ok", "code": code, @@ -132,6 +133,7 @@ def _state(self, params: dict[str, list[str]]) -> dict[str, Any]: "data": { "whoami": actions.to_json(who), "status": actions.to_json(rows), + "recent_history": actions.to_json(recent), "branches": actions.to_json(branches), "prs": actions.to_json(prs), }, @@ -586,7 +588,7 @@ def find_free_port(host: str = DEFAULT_HOST) -> int: