feat(ui): interactive graph, strip context menus, days by activity, message times, limit status words
This commit is contained in:
1 parent
025f2dbc3f
commit
c6401f1114
27 files changed
+1619
-381
No files matched your search
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||
from beaver_gateway.agents.claude import ClaudeAgent
|
||||
from beaver_gateway.conversations.kinds import Kind
|
||||
|
||||
__all__ = ["compose", "files_touched", "granules", "skill_sets"]
|
||||
__all__ = ["bash_paths", "compose", "files_touched", "granules", "skill_sets"]
|
||||
|
||||
CHARS_PER_TOKEN = 3.2
|
||||
READ_TOOLS = frozenset({"Read", "NotebookRead"})
|
||||
@@ -21,6 +21,40 @@ WRITE_TOOLS = frozenset({"Write", "Edit", "MultiEdit", "NotebookEdit"})
|
||||
SEARCH_TOOLS = frozenset({"Glob", "Grep"})
|
||||
PATH_KEYS = ("file_path", "notebook_path", "path", "file", "filename")
|
||||
_FRONTMATTER = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)
|
||||
# A shell command names files as quoted strings or bare words; only notes
|
||||
# and plain text files count, code and binaries are the agent's own business.
|
||||
_SHELL_TOKEN = re.compile(r'"([^"\n]+)"|\'([^\'\n]+)\'|([^\s"\'`;|&()<>]+)')
|
||||
_NOTE_SUFFIXES = (".md", ".canvas", ".txt", ".csv", ".json", ".yaml", ".yml")
|
||||
_SHELL_CD = re.compile(r'(?:^|&&|;|\|\|)\s*cd\s+("([^"]+)"|\'([^\']+)\'|(\S+))')
|
||||
_SHELL_WRITES = re.compile(
|
||||
r"write_text|write_bytes|>>?\s*[\"\']?[^\s&|]|\btee\b|\bsed\s+-i|\bmv\b|\brm\b"
|
||||
r"|\btouch\b|\bcp\b|open\([^)]*[\"\'][wa]"
|
||||
)
|
||||
|
||||
|
||||
def bash_paths(command: str, cwd: Path | None) -> tuple[list[str], Path | None, bool]:
|
||||
"""Files a shell command names, where it leaves the shell, whether it writes.
|
||||
|
||||
Relative names resolve against ``cwd``, or against the directory of a
|
||||
leading ``cd`` in the same command. Absolute names stay as they are.
|
||||
"""
|
||||
here = cwd
|
||||
for match in _SHELL_CD.finditer(command):
|
||||
target = match.group(2) or match.group(3) or match.group(4) or ""
|
||||
target = target.strip()
|
||||
if not target:
|
||||
continue
|
||||
here = Path(target) if target.startswith("/") else (here or Path()) / target
|
||||
found: list[str] = []
|
||||
for match in _SHELL_TOKEN.finditer(command):
|
||||
raw = (match.group(1) or match.group(2) or match.group(3) or "").strip()
|
||||
if not raw.lower().endswith(_NOTE_SUFFIXES) or "*" in raw:
|
||||
continue
|
||||
path = Path(raw) if raw.startswith("/") else (here or Path()) / raw
|
||||
text = str(path)
|
||||
if text not in found:
|
||||
found.append(text)
|
||||
return found, here, bool(_SHELL_WRITES.search(command))
|
||||
|
||||
|
||||
def _tokens(chars: int) -> int:
|
||||
@@ -56,27 +90,38 @@ def files_touched(
|
||||
"""Files named in tool inputs, with how they were touched, plus tool counts."""
|
||||
files: dict[str, dict[str, Any]] = {}
|
||||
counts: Counter[str] = Counter()
|
||||
# The shell keeps its directory between calls; follow it.
|
||||
shell_cwd = cwd
|
||||
|
||||
def touch(raw: str, how: str, stamp: str) -> None:
|
||||
key = _relative(raw, cwd)
|
||||
row = files.setdefault(
|
||||
key, {"path": key, "reads": 0, "writes": 0, "other": 0, "last_at": stamp}
|
||||
)
|
||||
row[how] += 1
|
||||
row["last_at"] = max(row["last_at"], stamp)
|
||||
|
||||
for name, tool_input, stamp in _tool_uses(entries):
|
||||
counts[name] += 1
|
||||
if not isinstance(tool_input, dict):
|
||||
continue
|
||||
if name == "Bash" and isinstance(tool_input.get("command"), str):
|
||||
paths, shell_cwd, writes = bash_paths(tool_input["command"], shell_cwd)
|
||||
for raw in paths:
|
||||
touch(raw, "writes" if writes else "reads", stamp)
|
||||
continue
|
||||
raw = next(
|
||||
(v for k in PATH_KEYS if isinstance(v := tool_input.get(k), str) and v),
|
||||
None,
|
||||
)
|
||||
if raw is None or (name in SEARCH_TOOLS and "file_path" not in tool_input):
|
||||
continue
|
||||
key = _relative(raw, cwd)
|
||||
row = files.setdefault(
|
||||
key, {"path": key, "reads": 0, "writes": 0, "other": 0, "last_at": stamp}
|
||||
)
|
||||
if name in READ_TOOLS:
|
||||
row["reads"] += 1
|
||||
touch(raw, "reads", stamp)
|
||||
elif name in WRITE_TOOLS:
|
||||
row["writes"] += 1
|
||||
touch(raw, "writes", stamp)
|
||||
else:
|
||||
row["other"] += 1
|
||||
row["last_at"] = max(row["last_at"], stamp)
|
||||
touch(raw, "other", stamp)
|
||||
ordered = sorted(files.values(), key=lambda r: r["last_at"], reverse=True)
|
||||
return ordered, dict(counts)
|
||||
|
||||
|
||||
Reference in new issue
Block a user