Skip to content

Commit 2842f34

Browse files
committed
feat: add web ui result summary cards
1 parent aeb3dd0 commit 2842f34

6 files changed

Lines changed: 178 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## 0.29.0 - 2026-04-06
3+
## 0.30.0 - 2026-04-06
44

55
- added a codex-repo adapter for recursive AGENTS.md and AGENTS.override.md repository scopes
66
- added fixtures and roundtrip coverage for Codex repository instruction layouts
@@ -176,3 +176,4 @@
176176

177177

178178

179+

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,5 @@ See `CONTRIBUTING.md`.
164164
MIT
165165

166166

167+
168+

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "memory-migrate-plugin"
7-
version = "0.29.0"
7+
version = "0.30.0"
88
description = "Open-source toolkit for migrating memories across AI agent systems."
99
readme = "README.md"
1010
requires-python = ">=3.11"
@@ -33,3 +33,4 @@ package-dir = {"" = "src"}
3333
where = ["src"]
3434

3535

36+
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
__all__ = ["__version__"]
22

3-
__version__ = "0.29.0"
3+
__version__ = "0.30.0"
4+
45

56

src/memory_migrate_plugin/serve.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,44 @@
199199
padding-top: 0;
200200
}
201201
.history-item strong { color: var(--ink); }
202+
.summary-grid {
203+
display: grid;
204+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
205+
gap: 12px;
206+
}
207+
.summary-card {
208+
padding: 14px;
209+
border-radius: 14px;
210+
border: 1px solid var(--line);
211+
background: #fffdf8;
212+
display: grid;
213+
gap: 6px;
214+
}
215+
.summary-card.ok {
216+
border-color: rgba(15, 118, 110, 0.28);
217+
background: rgba(15, 118, 110, 0.07);
218+
}
219+
.summary-card.warn {
220+
border-color: rgba(180, 83, 9, 0.32);
221+
background: rgba(180, 83, 9, 0.08);
222+
}
223+
.summary-card.error {
224+
border-color: rgba(185, 28, 28, 0.28);
225+
background: rgba(185, 28, 28, 0.08);
226+
}
227+
.summary-label {
228+
color: var(--muted);
229+
font-size: 0.82rem;
230+
text-transform: uppercase;
231+
letter-spacing: 0.08em;
232+
}
233+
.summary-value {
234+
color: var(--ink);
235+
font-size: 1.1rem;
236+
font-weight: 700;
237+
line-height: 1.2;
238+
word-break: break-word;
239+
}
202240
.downloads a {
203241
color: var(--accent);
204242
font-weight: 700;
@@ -295,6 +333,7 @@
295333
</div>
296334
<div class="card output">
297335
<div class="banner $status_class">$message</div>
336+
$summary_cards
298337
$download_links
299338
$history_panel
300339
<pre>$output</pre>
@@ -442,6 +481,105 @@ def render_history_panel(history: list[dict[str, Any]] | None = None, recent_dow
442481
return ''.join(sections)
443482

444483

484+
def summarize_action_result(action: str, result: dict[str, Any] | None) -> list[dict[str, str]]:
485+
if not result:
486+
return []
487+
payload = result.get("result", {})
488+
cards: list[dict[str, str]] = []
489+
490+
def add(label: str, value: Any, tone: str = "") -> None:
491+
if value is None:
492+
return
493+
rendered = str(value)
494+
if not rendered.strip():
495+
return
496+
cards.append({"label": label, "value": rendered, "tone": tone})
497+
498+
if action == "detect":
499+
matches = payload.get("matches", [])
500+
add("Matches", len(matches), "ok")
501+
if matches:
502+
add("Top Format", matches[0][0], "ok")
503+
add("Confidence", f"{matches[0][1]}%")
504+
return cards
505+
506+
if action == "inspect":
507+
add("Package", payload.get("package_id"), "ok")
508+
add("Entries", payload.get("entry_count"), "ok")
509+
add("Kinds", len(payload.get("kinds", [])))
510+
return cards
511+
512+
if action == "normalize":
513+
add("Package", payload.get("package_id"), "ok")
514+
add("Entries", payload.get("entry_count"), "ok")
515+
add("Output", Path(payload.get("output_path", "")).name if payload.get("output_path") else None)
516+
return cards
517+
518+
if action == "validate":
519+
summary = payload.get("summary", {})
520+
is_ok = bool(payload.get("ok"))
521+
add("Status", "Valid" if is_ok else "Invalid", "ok" if is_ok else "error")
522+
add("Errors", summary.get("error_count", 0), "error" if summary.get("error_count", 0) else "ok")
523+
add("Warnings", summary.get("warning_count", 0), "warn" if summary.get("warning_count", 0) else "ok")
524+
add("Entries", summary.get("entry_count", 0))
525+
return cards
526+
527+
if action == "report":
528+
audit = payload.get("audit", {})
529+
add("Package", payload.get("package_id"), "ok")
530+
add("Entries", payload.get("entry_count", 0), "ok")
531+
add("Issues", audit.get("issues_found", 0), "warn" if audit.get("issues_found", 0) else "ok")
532+
add("Formats", len(payload.get("source_formats", [])))
533+
return cards
534+
535+
if action == "doctor":
536+
summary = payload.get("doctor_summary", {})
537+
add("Health", summary.get("health_score", 0), "ok" if summary.get("health_score", 0) >= 80 else "warn")
538+
add("Issues", summary.get("issue_count", 0), "warn" if summary.get("issue_count", 0) else "ok")
539+
add("Suggestions", summary.get("suggestion_count", 0), "warn" if summary.get("suggestion_count", 0) else "ok")
540+
add("Repairable", summary.get("repairable_entry_count", 0))
541+
return cards
542+
543+
if action == "suggest":
544+
add("Suggestions", payload.get("suggestion_count", 0), "warn" if payload.get("suggestion_count", 0) else "ok")
545+
if payload.get("suggestions"):
546+
add("Top Severity", payload["suggestions"][0].get("severity", "unknown"))
547+
return cards
548+
549+
if action == "bundle":
550+
add("Entries", payload.get("export_entry_count") or payload.get("entry_count"), "ok")
551+
add("Target", payload.get("output", {}).get("target_format") if isinstance(payload.get("output"), dict) else payload.get("target_format"), "ok")
552+
doctor_summary = payload.get("doctor_summary", {})
553+
add("Health", doctor_summary.get("health_score"), "warn" if doctor_summary.get("health_score", 100) < 80 else "ok")
554+
add("Downloads", len(result.get("downloads", [])), "ok")
555+
return cards
556+
557+
if action == "schema":
558+
add("Schema", payload.get("title"), "ok")
559+
add("Version", payload.get("properties", {}).get("schema_version", {}).get("default", "1.0"))
560+
add("Entry Fields", len(payload.get("$defs", {}).get("memoryEntry", {}).get("required", [])))
561+
return cards
562+
563+
return cards
564+
565+
566+
def render_summary_cards(cards: list[dict[str, str]] | None) -> str:
567+
if not cards:
568+
return ""
569+
rows = ['<div class="summary-grid">']
570+
for item in cards:
571+
tone = str(item.get("tone", "")).strip()
572+
tone_class = f" {tone}" if tone else ""
573+
rows.append(
574+
f'<div class="summary-card{tone_class}">'
575+
f'<div class="summary-label">{_html_escape(item["label"])}</div>'
576+
f'<div class="summary-value">{_html_escape(item["value"])}</div>'
577+
'</div>'
578+
)
579+
rows.append('</div>')
580+
return ''.join(rows)
581+
582+
445583
def render_download_links(downloads: list[dict[str, str]] | None) -> str:
446584
if not downloads:
447585
return ""
@@ -565,11 +703,13 @@ def _handle_run(self) -> None:
565703
message = f"Action '{action}' completed successfully."
566704
status_class = "ok"
567705
downloads = result.get("downloads", [])
706+
summary_cards = summarize_action_result(action, result)
568707
output = json.dumps(result, indent=2, ensure_ascii=False)
569708
record_action_history(action, True, input_path, source_format, target_format, output_path, message, downloads)
570709
except Exception as exc:
571710
message = f"Action '{action}' failed: {exc}"
572711
status_class = "error"
712+
summary_cards = []
573713
output = json.dumps({"ok": False, "action": action, "error": str(exc)}, indent=2, ensure_ascii=False)
574714
record_action_history(action, False, input_path, source_format, target_format, output_path, message, [])
575715
self._send_html(
@@ -585,6 +725,7 @@ def _handle_run(self) -> None:
585725
status_class=status_class,
586726
output=output,
587727
downloads=downloads,
728+
summary_cards=summary_cards,
588729
)
589730
)
590731

@@ -612,6 +753,11 @@ def _handle_upload(self) -> None:
612753
message=message,
613754
status_class="ok",
614755
output=json.dumps(saved, indent=2, ensure_ascii=False),
756+
summary_cards=[
757+
{"label": "Upload", "value": Path(saved["zip_path"]).name, "tone": "ok"},
758+
{"label": "Workspace", "value": saved["workspace_id"]},
759+
{"label": "Extracted", "value": Path(saved["input_path"]).name or saved["input_path"]},
760+
],
615761
)
616762
)
617763
except Exception as exc:
@@ -657,6 +803,7 @@ def render_page(
657803
output: str = '{\n "status": "idle"\n}',
658804
downloads: list[dict[str, str]] | None = None,
659805
history: list[dict[str, Any]] | None = None,
806+
summary_cards: list[dict[str, str]] | None = None,
660807
) -> str:
661808
adapters = sorted(build_registry())
662809
profiles = sorted(list_profiles())
@@ -674,6 +821,7 @@ def render_page(
674821
message=_html_escape(message),
675822
status_class=_html_escape(status_class),
676823
output=_html_escape(output),
824+
summary_cards=render_summary_cards(summary_cards),
677825
download_links=render_download_links(downloads),
678826
history_panel=render_history_panel(history, _recent_downloads()),
679827
)

tests/test_roundtrip.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from memory_migrate_plugin.schema import build_canonical_package_schema, write_canonical_package_schema
2222
from memory_migrate_plugin.validate import validate_package, validate_package_file
2323
from memory_migrate_plugin.init_adapter import init_adapter
24-
from memory_migrate_plugin.serve import ACTION_HISTORY, DOWNLOAD_REGISTRY, execute_web_action, record_action_history, register_download, render_history_panel, render_page, save_uploaded_zip
24+
from memory_migrate_plugin.serve import ACTION_HISTORY, DOWNLOAD_REGISTRY, execute_web_action, record_action_history, register_download, render_history_panel, render_page, render_summary_cards, save_uploaded_zip, summarize_action_result
2525

2626

2727
class MemoryMigrateTests(unittest.TestCase):
@@ -449,6 +449,26 @@ def test_record_action_history_keeps_recent_entries_only(self) -> None:
449449
self.assertEqual(len(ACTION_HISTORY), 12)
450450
self.assertEqual(ACTION_HISTORY[0]["action"], "action-3")
451451

452+
def test_summarize_action_result_for_validate_has_status_cards(self) -> None:
453+
result = {
454+
"ok": True,
455+
"action": "validate",
456+
"result": {
457+
"ok": False,
458+
"summary": {"error_count": 2, "warning_count": 1, "entry_count": 3, "duplicate_id_count": 1},
459+
},
460+
}
461+
cards = summarize_action_result("validate", result)
462+
labels = [item["label"] for item in cards]
463+
self.assertIn("Status", labels)
464+
self.assertIn("Errors", labels)
465+
466+
def test_render_summary_cards_outputs_card_markup(self) -> None:
467+
html = render_summary_cards([{"label": "Health", "value": "95", "tone": "ok"}])
468+
self.assertIn("summary-card ok", html)
469+
self.assertIn("Health", html)
470+
self.assertIn("95", html)
471+
452472
def test_render_history_panel_shows_activity_and_downloads(self) -> None:
453473
ACTION_HISTORY.clear()
454474
DOWNLOAD_REGISTRY.clear()
@@ -489,6 +509,7 @@ def test_render_page_contains_ui_shell(self) -> None:
489509
self.assertIn("Agent Memory Bridge", html)
490510
self.assertIn("Run Workflow", html)
491511
self.assertIn("Recent Activity", html)
512+
self.assertIn("summary-grid", render_page(summary_cards=[{"label": "Entries", "value": "3", "tone": "ok"}]))
492513

493514
def test_execute_web_action_detect_returns_matches(self) -> None:
494515
result = execute_web_action("detect", "fixtures/generic-json/sample.json")

0 commit comments

Comments
 (0)