Skip to content

Commit 3f24a4e

Browse files
Copilothiyouga
andauthored
feat: add sort by name / modified time to tracer directory listing (#116)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: hiyouga <16256802+hiyouga@users.noreply.github.com> Co-authored-by: hiyouga <hiyouga@buaa.edu.cn>
1 parent 2c715b4 commit 3f24a4e

6 files changed

Lines changed: 551 additions & 38 deletions

File tree

src_py/agenthub/integration/tracer.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from pathlib import Path
3030
from typing import Any
3131

32-
from flask import Flask, Response, render_template_string
32+
from flask import Flask, Response, render_template_string, request
3333

3434
from ..types import UniMessage
3535

@@ -330,6 +330,11 @@ def inline_thinking_summary(item: dict[str, Any]) -> str:
330330
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
331331
<p class="text-sm text-gray-600"><strong>Path:</strong> {{ breadcrumb|safe }}</p>
332332
</div>
333+
<div class="flex items-center gap-2 mb-3">
334+
<span class="text-xs text-gray-500">Sort by:</span>
335+
<a href="{{ sort_name_url }}" class="px-3 py-1 text-xs rounded border transition-colors {% if current_sort == 'name' %}bg-blue-600 text-white border-blue-600{% else %}bg-white text-gray-700 border-gray-300 hover:bg-gray-50{% endif %}">Name</a>
336+
<a href="{{ sort_mtime_url }}" class="px-3 py-1 text-xs rounded border transition-colors {% if current_sort == 'mtime' %}bg-blue-600 text-white border-blue-600{% else %}bg-white text-gray-700 border-gray-300 hover:bg-gray-50{% endif %}">Modified Time</a>
337+
</div>
333338
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
334339
{% if items %}
335340
{% for item in items %}
@@ -339,9 +344,14 @@ def inline_thinking_summary(item: dict[str, Any]) -> str:
339344
<span class="mr-2">{% if item.is_dir %}📁{% else %}📄{% endif %}</span>
340345
<span class="text-sm">{{ item.name }}</span>
341346
</span>
342-
{% if item.size %}
343-
<span class="text-xs text-gray-500">{{ item.size }}</span>
344-
{% endif %}
347+
<span class="flex items-center gap-4">
348+
{% if item.mtime %}
349+
<span class="text-xs text-gray-400">{{ item.mtime }}</span>
350+
{% endif %}
351+
{% if item.size %}
352+
<span class="text-xs text-gray-500">{{ item.size }}</span>
353+
{% endif %}
354+
</span>
345355
</a>
346356
</div>
347357
{% endfor %}
@@ -635,9 +645,19 @@ def browse(subpath: str = "") -> str | Response:
635645
return f"Error reading file: {str(e)}", 500
636646

637647
# If it's a directory, list its contents
648+
sort_by = request.args.get("sort", "name")
649+
if sort_by not in ("name", "mtime"):
650+
sort_by = "name"
651+
638652
items = []
639653
try:
640-
for entry in sorted(full_path.iterdir(), key=lambda x: (not x.is_dir(), x.name)):
654+
entries = list(full_path.iterdir())
655+
if sort_by == "mtime":
656+
entries.sort(key=lambda x: (not x.is_dir(), -x.stat().st_mtime))
657+
else:
658+
entries.sort(key=lambda x: (not x.is_dir(), x.name))
659+
660+
for entry in entries:
641661
# Calculate relative path from cache_dir
642662
try:
643663
relative_path = entry.resolve().relative_to(self.cache_dir.resolve())
@@ -648,6 +668,7 @@ def browse(subpath: str = "") -> str | Response:
648668
"name": entry.name,
649669
"is_dir": entry.is_dir(),
650670
"url": f"/{relative_path}",
671+
"mtime": datetime.fromtimestamp(entry.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
651672
}
652673
if entry.is_file():
653674
size = entry.stat().st_size
@@ -670,7 +691,18 @@ def browse(subpath: str = "") -> str | Response:
670691
breadcrumb_parts.append(f'<a href="/{path_to_part}">{part}</a>')
671692
breadcrumb = " / ".join(breadcrumb_parts)
672693

673-
return render_template_string(DIRECTORY_TEMPLATE, items=items, breadcrumb=breadcrumb)
694+
base_url = "/" + subpath if subpath else "/"
695+
sort_name_url = base_url + "?sort=name"
696+
sort_mtime_url = base_url + "?sort=mtime"
697+
698+
return render_template_string(
699+
DIRECTORY_TEMPLATE,
700+
items=items,
701+
breadcrumb=breadcrumb,
702+
current_sort=sort_by,
703+
sort_name_url=sort_name_url,
704+
sort_mtime_url=sort_mtime_url,
705+
)
674706

675707
return app
676708

src_py/tests/test_tracer.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,91 @@ def test_format_config_with_system_and_tools(temp_cache_dir):
330330
assert "tools:" in txt_content
331331
assert "get_weather" in txt_content
332332
assert "parameters" in txt_content
333+
334+
335+
def test_web_app_sort_by_name(temp_cache_dir):
336+
"""Test directory listing sorted by name."""
337+
tracer = Tracer(cache_dir=temp_cache_dir)
338+
339+
model = "fake-model"
340+
history = [{"role": "user", "content_items": [{"type": "text", "text": "Test"}]}]
341+
config = {}
342+
tracer.save_history(model, history, "zebra/conv", config)
343+
tracer.save_history(model, history, "apple/conv", config)
344+
tracer.save_history(model, history, "mango/conv", config)
345+
346+
app = tracer.create_web_app()
347+
348+
with app.test_client() as client:
349+
response = client.get("/?sort=name")
350+
assert response.status_code == 200
351+
html = response.data.decode()
352+
353+
# Dirs should appear alphabetically: apple, mango, zebra
354+
pos_apple = html.index("apple")
355+
pos_mango = html.index("mango")
356+
pos_zebra = html.index("zebra")
357+
assert pos_apple < pos_mango < pos_zebra
358+
359+
# Sort controls should be present
360+
assert "sort=name" in html
361+
assert "sort=mtime" in html
362+
363+
364+
def test_web_app_sort_by_mtime(temp_cache_dir):
365+
"""Test directory listing sorted by modification time (most recent first)."""
366+
tracer = Tracer(cache_dir=temp_cache_dir)
367+
368+
model = "fake-model"
369+
history = [{"role": "user", "content_items": [{"type": "text", "text": "Test"}]}]
370+
config = {}
371+
372+
tracer.save_history(model, history, "alpha/conv", config)
373+
tracer.save_history(model, history, "beta/conv", config)
374+
tracer.save_history(model, history, "gamma/conv", config)
375+
376+
# Explicitly set directory mtimes so the order is deterministic
377+
alpha_dir = Path(temp_cache_dir) / "alpha"
378+
beta_dir = Path(temp_cache_dir) / "beta"
379+
gamma_dir = Path(temp_cache_dir) / "gamma"
380+
os.utime(alpha_dir, (1000, 1000))
381+
os.utime(beta_dir, (2000, 2000))
382+
os.utime(gamma_dir, (3000, 3000))
383+
384+
app = tracer.create_web_app()
385+
386+
with app.test_client() as client:
387+
response = client.get("/?sort=mtime")
388+
assert response.status_code == 200
389+
html = response.data.decode()
390+
391+
# Most recently modified directory (gamma, mtime=3000) should appear before alpha (mtime=1000)
392+
pos_gamma = html.index("gamma")
393+
pos_alpha = html.index("alpha")
394+
assert pos_gamma < pos_alpha
395+
396+
# Sort controls should be present
397+
assert "sort=name" in html
398+
assert "sort=mtime" in html
399+
400+
401+
def test_web_app_sort_default_is_name(temp_cache_dir):
402+
"""Test that the default sort order is by name."""
403+
tracer = Tracer(cache_dir=temp_cache_dir)
404+
405+
model = "fake-model"
406+
history = [{"role": "user", "content_items": [{"type": "text", "text": "Test"}]}]
407+
config = {}
408+
tracer.save_history(model, history, "zebra/conv", config)
409+
tracer.save_history(model, history, "apple/conv", config)
410+
411+
app = tracer.create_web_app()
412+
413+
with app.test_client() as client:
414+
response = client.get("/")
415+
assert response.status_code == 200
416+
html = response.data.decode()
417+
418+
pos_apple = html.index("apple")
419+
pos_zebra = html.index("zebra")
420+
assert pos_apple < pos_zebra

0 commit comments

Comments
 (0)