clone

a hermes agent plugin for openspec: tools for spec workflows and a dashboard tab for browsing changes and specs

Show created + edited dates in spec detail

dami etoile committed 8549c93
commit
8549c93aa9be64389700b685ff45ab533d5fa1e7
parent
26718c2bc326c54a56664d42acd99a98293b303a
author
dami etoile <dami.etoile@gmail.com>
3 files changed, +29 -11
Mdashboard/dist/index.js5++++
Mdashboard/dist/style.css1+
Mdashboard/plugin_api.py34++++++++++++++++++++----------
M dashboard/dist/index.js +5 -0
M dashboard/dist/style.css +1 -0
M dashboard/plugin_api.py +23 -11
@@ -476,11 +476,13 @@ def _worktree_spec_mtime(root: Path, spec_path: str) -> Optional[str]:
476476 return None477477 478478 479-def _spec_git_mtimes(root: Path) -> dict[str, str]:480- """Return ``{rel_path: iso_date}`` for the last commit touching each spec.479+def _spec_git_dates(root: Path) -> dict[str, dict[str, str]]:480+ """Return ``{rel_path: {"mtime": iso, "ctime": iso}}`` for each spec.481481 482- Uses a single ``git log`` call and returns the most recent commit date483- per file. Falls back to an empty dict if git is unavailable.482+ Uses a single ``git log`` call (newest-first). The first time a file483+ appears is its most recent commit (mtime); the last time is the oldest484+ commit (ctime / creation date). Falls back to an empty dict if git is485+ unavailable.484486 """485487 text = _git_output(486488 root,
@@ -490,18 +492,22 @@ def _spec_git_mtimes(root: Path) -> dict[str, str]:
490492 if not text:491493 return {}492494 prefix = "openspec/specs/"493- mtimes: dict[str, str] = {}495+ dates: dict[str, dict[str, str]] = {}494496 current_date: Optional[str] = None495497 for line in text.splitlines():496498 if not line:497499 continue498500 if line.startswith(prefix):499501 rel = line[len(prefix):]500- if rel.endswith(".md") and current_date and rel not in mtimes:501- mtimes[rel] = current_date502+ if not rel.endswith(".md") or not current_date:503+ continue504+ if rel not in dates:505+ dates[rel] = {"mtime": current_date, "ctime": current_date}506+ else:507+ dates[rel]["ctime"] = current_date502508 else:503509 current_date = line.strip()504- return mtimes510+ return dates505511 506512 507513 def _read_ref_spec(root: Path, ref: str, spec_path: str) -> Optional[str]:
@@ -571,18 +577,20 @@ def _spec_browser(root: Path, before: str = "", after: str = "", dirty: bool = F
571577 572578 files: list[dict[str, Any]] = []573579 # Pre-fetch git commit dates for all specs in one call (current/dirty modes).574- git_mtimes = _spec_git_mtimes(root) if mode in ("current", "dirty") else {}580+ git_dates = _spec_git_dates(root) if mode in ("current", "dirty") else {}575581 for rel in sorted(set(before_paths) | set(after_paths)):576582 if mode == "current":577583 # No comparison — just showing the current worktree state.578584 after_content = _read_worktree_spec(root, rel)585+ gd = git_dates.get(rel, {})579586 files.append({580587 "path": rel,581588 "token": _change_token(f"spec:{rel}"),582589 "title": _spec_title_from_content(after_content, rel),583590 "status": "current" if after_content is not None else "missing",584591 "changed": False,585- "mtime": git_mtimes.get(rel) or _worktree_spec_mtime(root, rel),592+ "mtime": gd.get("mtime") or _worktree_spec_mtime(root, rel),593+ "ctime": gd.get("ctime"),586594 "before": None,587595 "after": after_content,588596 "diff": "",
@@ -591,10 +599,13 @@ def _spec_browser(root: Path, before: str = "", after: str = "", dirty: bool = F
591599 before_content = _read_ref_spec(root, before_ref, rel) if before_ref else None592600 if dirty_mode:593601 after_content = _read_worktree_spec(root, rel)594- mtime = git_mtimes.get(rel) or _worktree_spec_mtime(root, rel)602+ gd = git_dates.get(rel, {})603+ mtime = gd.get("mtime") or _worktree_spec_mtime(root, rel)604+ ctime = gd.get("ctime")595605 else:596606 after_content = _read_ref_spec(root, after_ref, rel)597607 mtime = None608+ ctime = None598609 status = _spec_status(before_content, after_content)599610 files.append({600611 "path": rel,
@@ -603,6 +614,7 @@ def _spec_browser(root: Path, before: str = "", after: str = "", dirty: bool = F
603614 "status": status,604615 "changed": status not in {"unchanged", "missing"},605616 "mtime": mtime,617+ "ctime": ctime,606618 "before": before_content,607619 "after": after_content,608620 "diff": _spec_diff(before_content, after_content, rel) if status not in {"unchanged", "missing"} else "",