- commit
- 8549c93aa9be64389700b685ff45ab533d5fa1e7
- parent
- 26718c2bc326c54a56664d42acd99a98293b303a
- author
- dami etoile <dami.etoile@gmail.com>
| M | dashboard/ | 5 | ++++ |
| M | dashboard/ | 1 | + |
| M | dashboard/ | 34 | ++++++++++++++++++++---------- |
@@ -23,6 +23,7 @@
2323 var useState = SDK.hooks.useState, useEffect = SDK.hooks.useEffect;2424 var useCallback = SDK.hooks.useCallback, useMemo = SDK.hooks.useMemo, useRef = SDK.hooks.useRef;2525 var cn = SDK.utils.cn;26+ var isoTimeAgo = SDK.utils.isoTimeAgo;2627 var fetchJSON = SDK.fetchJSON;2728 2829 var BASE = "/api/plugins/openspec";@@ -425,6 +426,10 @@
425426 h("span", { className: "os-specs-detail-path" }, selFile.path),426427 selFile.token ? h(CopyChip, { text: source.name + "/" + selFile.token }) : null427428 ),429+ (selFile.ctime || selFile.mtime) ? h("div", { className: "os-specs-meta" },430+ selFile.ctime ? h("span", { title: selFile.ctime }, "created ", isoTimeAgo(selFile.ctime)) : null,431+ selFile.mtime ? h("span", { title: selFile.mtime }, "edited ", isoTimeAgo(selFile.mtime)) : null432+ ) : null,428433 mode === "current" ? h(Markdown, { md: selFile.after }) :429434 h("div", { className: "os-spec-diff" },430435 h("div", { className: "os-diff-cols" },@@ -120,6 +120,7 @@
120120 .os-specs-detail { border: 1px solid var(--color-border, #2a2a2a); border-radius: var(--radius, 0.5rem); padding: 0.75rem; overflow-y: auto; max-height: calc(100vh - 14rem); }121121 .os-specs-detail-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }122122 .os-specs-detail-path { font-family: var(--theme-font-mono, monospace); font-size: 0.8rem; font-weight: 500; color: var(--color-foreground, #eee); }123+.os-specs-meta { display: flex; gap: 0.75rem; margin-bottom: 0.5rem; font-size: 0.7rem; color: var(--color-muted-foreground, #888); }123124 .os-diff-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 0.75rem; }124125 .os-diff-col { border: 1px solid var(--color-border, #2a2a2a); border-radius: var(--radius-sm, 0.375rem); padding: 0.5rem; overflow-x: auto; }125126 .os-diff-col-head { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--color-muted-foreground, #777); margin-bottom: 0.375rem; }@@ -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 "",