2323
2424UI_WORKSPACE_ROOT = Path (tempfile .gettempdir ()) / "agent-memory-bridge-ui"
2525DOWNLOAD_REGISTRY : dict [str , Path ] = {}
26+ ACTION_HISTORY : list [dict [str , Any ]] = []
27+ MAX_ACTION_HISTORY = 12
28+ MAX_RECENT_DOWNLOADS = 10
2629
2730HTML_PAGE = Template ("""<!doctype html>
2831<html lang="en">
171174 background: #fffdf8;
172175 border: 1px solid var(--line);
173176 }
177+ .panel-title {
178+ font-weight: 700;
179+ letter-spacing: 0.02em;
180+ }
181+ .history {
182+ display: grid;
183+ gap: 10px;
184+ padding: 14px;
185+ border-radius: 14px;
186+ background: #fffdf8;
187+ border: 1px solid var(--line);
188+ }
189+ .history-item {
190+ border-top: 1px solid rgba(216, 201, 177, 0.7);
191+ padding-top: 10px;
192+ display: grid;
193+ gap: 4px;
194+ color: var(--muted);
195+ font-size: 0.92rem;
196+ }
197+ .history-item:first-of-type {
198+ border-top: none;
199+ padding-top: 0;
200+ }
201+ .history-item strong { color: var(--ink); }
174202 .downloads a {
175203 color: var(--accent);
176204 font-weight: 700;
268296 <div class="card output">
269297 <div class="banner $status_class">$message</div>
270298 $download_links
299+ $history_panel
271300 <pre>$output</pre>
272301 <div class="tips">
273302 <div>Suggested first try: <code>detect</code> or <code>inspect</code> before <code>bundle</code>.</div>
@@ -348,6 +377,71 @@ def register_download(path: Path) -> dict[str, str]:
348377 }
349378
350379
380+ def _recent_downloads () -> list [dict [str , str ]]:
381+ items : list [dict [str , str ]] = []
382+ for token , path in list (DOWNLOAD_REGISTRY .items ())[- MAX_RECENT_DOWNLOADS :][::- 1 ]:
383+ if path .exists () and path .is_file ():
384+ items .append ({
385+ "token" : token ,
386+ "path" : str (path ),
387+ "filename" : path .name ,
388+ "url" : f"/download?token={ token } " ,
389+ })
390+ return items
391+
392+
393+ def record_action_history (
394+ action : str ,
395+ ok : bool ,
396+ input_path : str ,
397+ source_format : str | None ,
398+ target_format : str | None ,
399+ output_path : str | None ,
400+ message : str ,
401+ downloads : list [dict [str , str ]] | None = None ,
402+ ) -> None :
403+ entry = {
404+ "id" : uuid .uuid4 ().hex [:8 ],
405+ "action" : action ,
406+ "ok" : ok ,
407+ "input_path" : input_path ,
408+ "source_format" : source_format or "auto" ,
409+ "target_format" : target_format or "-" ,
410+ "output_path" : output_path or "-" ,
411+ "message" : message ,
412+ "downloads" : list (downloads or []),
413+ }
414+ ACTION_HISTORY .append (entry )
415+ if len (ACTION_HISTORY ) > MAX_ACTION_HISTORY :
416+ del ACTION_HISTORY [:- MAX_ACTION_HISTORY ]
417+
418+
419+ def render_history_panel (history : list [dict [str , Any ]] | None = None , recent_downloads : list [dict [str , str ]] | None = None ) -> str :
420+ history = history if history is not None else ACTION_HISTORY
421+ recent_downloads = recent_downloads if recent_downloads is not None else _recent_downloads ()
422+ sections = ['<div class="history"><div class="panel-title">Recent Activity</div>' ]
423+ if not history :
424+ sections .append ('<div class="history-item"><strong>No runs yet.</strong><div>Run a workflow to populate local session history.</div></div>' )
425+ else :
426+ for item in history [::- 1 ]:
427+ state = "ok" if item ["ok" ] else "error"
428+ sections .append (
429+ f'<div class="history-item"><strong>{ _html_escape (item ["action" ])} ? { state } </strong>'
430+ f'<div>{ _html_escape (item ["message" ])} </div>'
431+ f'<div>input: { _html_escape (item ["input_path" ])} </div>'
432+ f'<div>target: { _html_escape (item ["target_format" ])} ? output: { _html_escape (item ["output_path" ])} </div></div>'
433+ )
434+ if recent_downloads :
435+ sections .append ('<div class="panel-title">Recent Downloads</div>' )
436+ for item in recent_downloads :
437+ sections .append (
438+ f'<div class="history-item"><a href="{ _html_escape (item ["url" ])} ">{ _html_escape (item ["filename" ])} </a>'
439+ f'<div>{ _html_escape (item ["path" ])} </div></div>'
440+ )
441+ sections .append ('</div>' )
442+ return '' .join (sections )
443+
444+
351445def render_download_links (downloads : list [dict [str , str ]] | None ) -> str :
352446 if not downloads :
353447 return ""
@@ -472,10 +566,12 @@ def _handle_run(self) -> None:
472566 status_class = "ok"
473567 downloads = result .get ("downloads" , [])
474568 output = json .dumps (result , indent = 2 , ensure_ascii = False )
569+ record_action_history (action , True , input_path , source_format , target_format , output_path , message , downloads )
475570 except Exception as exc :
476571 message = f"Action '{ action } ' failed: { exc } "
477572 status_class = "error"
478573 output = json .dumps ({"ok" : False , "action" : action , "error" : str (exc )}, indent = 2 , ensure_ascii = False )
574+ record_action_history (action , False , input_path , source_format , target_format , output_path , message , [])
479575 self ._send_html (
480576 render_page (
481577 action = action ,
@@ -508,16 +604,20 @@ def _handle_upload(self) -> None:
508604 try :
509605 payload = upload .file .read ()
510606 saved = save_uploaded_zip (getattr (upload , "filename" , "upload.zip" ), payload )
607+ message = f"Uploaded and extracted zip into { saved ['input_path' ]} "
608+ record_action_history ("upload" , True , saved ["input_path" ], None , None , saved ["zip_path" ], message , [])
511609 self ._send_html (
512610 render_page (
513611 input_path = saved ["input_path" ],
514- message = f"Uploaded and extracted zip into { saved [ 'input_path' ] } " ,
612+ message = message ,
515613 status_class = "ok" ,
516614 output = json .dumps (saved , indent = 2 , ensure_ascii = False ),
517615 )
518616 )
519617 except Exception as exc :
520- self ._send_html (render_page (message = f"Upload failed: { exc } " , status_class = "error" ))
618+ message = f"Upload failed: { exc } "
619+ record_action_history ("upload" , False , "" , None , None , None , message , [])
620+ self ._send_html (render_page (message = message , status_class = "error" ))
521621
522622 def _send_download (self , token : str ) -> None :
523623 path = DOWNLOAD_REGISTRY .get (token )
@@ -556,6 +656,7 @@ def render_page(
556656 status_class : str = "" ,
557657 output : str = '{\n "status": "idle"\n }' ,
558658 downloads : list [dict [str , str ]] | None = None ,
659+ history : list [dict [str , Any ]] | None = None ,
559660) -> str :
560661 adapters = sorted (build_registry ())
561662 profiles = sorted (list_profiles ())
@@ -574,6 +675,7 @@ def render_page(
574675 status_class = _html_escape (status_class ),
575676 output = _html_escape (output ),
576677 download_links = render_download_links (downloads ),
678+ history_panel = render_history_panel (history , _recent_downloads ()),
577679 )
578680
579681
0 commit comments