Spaces:
Sleeping
Sleeping
feat: rename sessions using agent summary title, display narrative summary, and add folder-wise filter to sessions tab
Browse files- src/app.py +2 -3
- src/functions.py +21 -1
- src/viewer/index.html +50 -8
src/app.py
CHANGED
|
@@ -419,8 +419,7 @@ def api_replay_sessions():
|
|
| 419 |
if auth_err:
|
| 420 |
return auth_err
|
| 421 |
|
| 422 |
-
sessions =
|
| 423 |
-
sessions.sort(key=lambda s: s.get("startedAt", ""), reverse=True)
|
| 424 |
return jsonify({"success": True, "sessions": sessions}), 200
|
| 425 |
|
| 426 |
@app.route("/agentmemory/session/start", methods=["POST"])
|
|
@@ -1152,7 +1151,7 @@ def api_replay_load():
|
|
| 1152 |
session_id = request.args.get("sessionId")
|
| 1153 |
if not session_id:
|
| 1154 |
return jsonify({"error": "sessionId required"}), 400
|
| 1155 |
-
session =
|
| 1156 |
if not session:
|
| 1157 |
return jsonify({"error": "session not found"}), 404
|
| 1158 |
obs = kv.list(KV.observations(session_id))
|
|
|
|
| 419 |
if auth_err:
|
| 420 |
return auth_err
|
| 421 |
|
| 422 |
+
sessions = functions.list_sessions(kv)
|
|
|
|
| 423 |
return jsonify({"success": True, "sessions": sessions}), 200
|
| 424 |
|
| 425 |
@app.route("/agentmemory/session/start", methods=["POST"])
|
|
|
|
| 1151 |
session_id = request.args.get("sessionId")
|
| 1152 |
if not session_id:
|
| 1153 |
return jsonify({"error": "sessionId required"}), 400
|
| 1154 |
+
session = functions.get_session(kv, session_id)
|
| 1155 |
if not session:
|
| 1156 |
return jsonify({"error": "session not found"}), 404
|
| 1157 |
obs = kv.list(KV.observations(session_id))
|
src/functions.py
CHANGED
|
@@ -1709,11 +1709,24 @@ def rebuild_index(kv: StateKV) -> int:
|
|
| 1709 |
|
| 1710 |
def list_sessions(kv: StateKV) -> List[Dict[str, Any]]:
|
| 1711 |
sessions = kv.list(KV.sessions)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1712 |
sessions.sort(key=lambda s: s.get("startedAt", ""), reverse=True)
|
| 1713 |
return sessions
|
| 1714 |
|
| 1715 |
def get_session(kv: StateKV, session_id: str) -> Optional[Dict[str, Any]]:
|
| 1716 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1717 |
|
| 1718 |
def create_session(kv: StateKV, session: Dict[str, Any]) -> Dict[str, Any]:
|
| 1719 |
kv.set(KV.sessions, session["id"], session)
|
|
@@ -2150,6 +2163,13 @@ def summarize(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 2150 |
return {"success": False, "error": f"Reduction failed: {e}"}
|
| 2151 |
|
| 2152 |
kv.set(KV.summaries, session_id, final_summary)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2153 |
safe_audit(kv, "compress", "mem::summarize", [session_id], {
|
| 2154 |
"title": final_summary["title"],
|
| 2155 |
"observationCount": len(compressed)
|
|
|
|
| 1709 |
|
| 1710 |
def list_sessions(kv: StateKV) -> List[Dict[str, Any]]:
|
| 1711 |
sessions = kv.list(KV.sessions)
|
| 1712 |
+
for s in sessions:
|
| 1713 |
+
sid = s.get("id")
|
| 1714 |
+
if sid:
|
| 1715 |
+
summary = kv.get(KV.summaries, sid)
|
| 1716 |
+
if summary:
|
| 1717 |
+
s["title"] = summary.get("title")
|
| 1718 |
+
s["summary"] = summary.get("narrative")
|
| 1719 |
sessions.sort(key=lambda s: s.get("startedAt", ""), reverse=True)
|
| 1720 |
return sessions
|
| 1721 |
|
| 1722 |
def get_session(kv: StateKV, session_id: str) -> Optional[Dict[str, Any]]:
|
| 1723 |
+
s = kv.get(KV.sessions, session_id)
|
| 1724 |
+
if s:
|
| 1725 |
+
summary = kv.get(KV.summaries, session_id)
|
| 1726 |
+
if summary:
|
| 1727 |
+
s["title"] = summary.get("title")
|
| 1728 |
+
s["summary"] = summary.get("narrative")
|
| 1729 |
+
return s
|
| 1730 |
|
| 1731 |
def create_session(kv: StateKV, session: Dict[str, Any]) -> Dict[str, Any]:
|
| 1732 |
kv.set(KV.sessions, session["id"], session)
|
|
|
|
| 2163 |
return {"success": False, "error": f"Reduction failed: {e}"}
|
| 2164 |
|
| 2165 |
kv.set(KV.summaries, session_id, final_summary)
|
| 2166 |
+
|
| 2167 |
+
session = kv.get(KV.sessions, session_id)
|
| 2168 |
+
if session:
|
| 2169 |
+
session["title"] = final_summary["title"]
|
| 2170 |
+
session["summary"] = final_summary["narrative"]
|
| 2171 |
+
kv.set(KV.sessions, session_id, session)
|
| 2172 |
+
|
| 2173 |
safe_audit(kv, "compress", "mem::summarize", [session_id], {
|
| 2174 |
"title": final_summary["title"],
|
| 2175 |
"observationCount": len(compressed)
|
src/viewer/index.html
CHANGED
|
@@ -1259,7 +1259,7 @@
|
|
| 1259 |
graph: { loaded: false, nodes: [], edges: [], stats: null, filters: {}, selectedNode: null, queryError: null, truncated: false, totalNodes: 0, totalEdges: 0 },
|
| 1260 |
memories: { loaded: false, items: [], search: '', typeFilter: '' },
|
| 1261 |
timeline: { loaded: false, observations: [], sessionId: '', minImportance: 0, page: 0, pageSize: 50 },
|
| 1262 |
-
sessions: { loaded: false, items: [], selectedId: null },
|
| 1263 |
audit: { loaded: false, entries: [], opFilter: '' },
|
| 1264 |
activity: { loaded: false, observations: [], sessions: [], typeFilter: '' },
|
| 1265 |
lessons: { loaded: false, items: [], search: '' },
|
|
@@ -1299,8 +1299,15 @@
|
|
| 1299 |
return id ? id.slice(0, n || 8) : '';
|
| 1300 |
}
|
| 1301 |
function sessionDisplayName(s) {
|
| 1302 |
-
var
|
| 1303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1304 |
return shortSessionId(s, 8) || 'Unknown session';
|
| 1305 |
}
|
| 1306 |
function sessionLabel(s) {
|
|
@@ -2997,18 +3004,45 @@
|
|
| 2997 |
return (b.startedAt || '').localeCompare(a.startedAt || '');
|
| 2998 |
});
|
| 2999 |
|
| 3000 |
-
var
|
| 3001 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3002 |
html += '<div class="empty-state"><div class="empty-icon">🗒</div><p>No sessions</p></div>';
|
| 3003 |
} else {
|
| 3004 |
-
|
| 3005 |
var statusBadge = s.status === 'active' ? 'badge-green' : s.status === 'completed' ? 'badge-blue' : 'badge-muted';
|
| 3006 |
var id = sessionId(s);
|
| 3007 |
var selected = id && state.sessions.selectedId === id;
|
| 3008 |
html += '<div class="session-item' + (selected ? ' selected' : '') + '"' + (id ? ' data-action="select-session" data-session-id="' + esc(id) + '"' : '') + '>';
|
| 3009 |
html += '<div class="session-top"><span class="session-project">' + esc(sessionDisplayName(s)) + '</span>';
|
| 3010 |
html += '<span class="badge ' + statusBadge + '">' + esc(s.status) + '</span></div>';
|
| 3011 |
-
var preview = s.
|
| 3012 |
if (preview) {
|
| 3013 |
html += '<div class="session-preview" style="font-size:13px;color:var(--ink);margin:4px 0;line-height:1.4;">' + esc(truncate(preview, 140)) + '</div>';
|
| 3014 |
}
|
|
@@ -3022,6 +3056,14 @@
|
|
| 3022 |
html += '<div id="session-detail"></div>';
|
| 3023 |
el.innerHTML = html;
|
| 3024 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3025 |
if (state.sessions.selectedId) renderSessionDetail();
|
| 3026 |
}
|
| 3027 |
|
|
@@ -3060,7 +3102,7 @@
|
|
| 3060 |
var durationMs = s.endedAt ? new Date(s.endedAt).getTime() - new Date(s.startedAt).getTime() : 0;
|
| 3061 |
var durationLabel = durationMs > 0 ? (durationMs < 60000 ? (durationMs / 1000).toFixed(1) + 's' : (durationMs / 60000).toFixed(1) + 'm') : '-';
|
| 3062 |
|
| 3063 |
-
var preview = s.
|
| 3064 |
|
| 3065 |
var html = '<div class="detail-panel">';
|
| 3066 |
html += '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px;">';
|
|
|
|
| 1259 |
graph: { loaded: false, nodes: [], edges: [], stats: null, filters: {}, selectedNode: null, queryError: null, truncated: false, totalNodes: 0, totalEdges: 0 },
|
| 1260 |
memories: { loaded: false, items: [], search: '', typeFilter: '' },
|
| 1261 |
timeline: { loaded: false, observations: [], sessionId: '', minImportance: 0, page: 0, pageSize: 50 },
|
| 1262 |
+
sessions: { loaded: false, items: [], selectedId: null, folderFilter: '' },
|
| 1263 |
audit: { loaded: false, entries: [], opFilter: '' },
|
| 1264 |
activity: { loaded: false, observations: [], sessions: [], typeFilter: '' },
|
| 1265 |
lessons: { loaded: false, items: [], search: '' },
|
|
|
|
| 1299 |
return id ? id.slice(0, n || 8) : '';
|
| 1300 |
}
|
| 1301 |
function sessionDisplayName(s) {
|
| 1302 |
+
var folder = s && s.project ? String(s.project).split(/[\\/]/).pop() : '';
|
| 1303 |
+
var title = s && s.title ? String(s.title).trim() : '';
|
| 1304 |
+
if (title && folder) {
|
| 1305 |
+
return title + ' (' + folder + ')';
|
| 1306 |
+
} else if (title) {
|
| 1307 |
+
return title;
|
| 1308 |
+
} else if (folder) {
|
| 1309 |
+
return folder;
|
| 1310 |
+
}
|
| 1311 |
return shortSessionId(s, 8) || 'Unknown session';
|
| 1312 |
}
|
| 1313 |
function sessionLabel(s) {
|
|
|
|
| 3004 |
return (b.startedAt || '').localeCompare(a.startedAt || '');
|
| 3005 |
});
|
| 3006 |
|
| 3007 |
+
var folderFilter = state.sessions.folderFilter || '';
|
| 3008 |
+
var projects = [];
|
| 3009 |
+
state.sessions.items.forEach(function(s) {
|
| 3010 |
+
var folder = s.project ? String(s.project).split(/[\\/]/).pop() : '';
|
| 3011 |
+
if (folder && !projects.includes(folder)) {
|
| 3012 |
+
projects.push(folder);
|
| 3013 |
+
}
|
| 3014 |
+
});
|
| 3015 |
+
projects.sort();
|
| 3016 |
+
|
| 3017 |
+
var toolbarHtml = '<div class="toolbar" style="margin-bottom: 12px; display: flex; gap: 10px; align-items: center;">';
|
| 3018 |
+
toolbarHtml += '<span style="font-size: 12px; font-weight: 600; color: var(--ink-muted);">FILTER BY FOLDER:</span>';
|
| 3019 |
+
toolbarHtml += '<select id="sessions-folder-filter" style="width: 200px; padding: 6px 10px; border-radius: 4px; border: 1px solid var(--border); background: var(--bg); color: var(--ink);">';
|
| 3020 |
+
toolbarHtml += '<option value="">All Folders</option>';
|
| 3021 |
+
projects.forEach(function(p) {
|
| 3022 |
+
var selected = p === folderFilter ? ' selected' : '';
|
| 3023 |
+
toolbarHtml += '<option value="' + esc(p) + '"' + selected + '>' + esc(p) + '</option>';
|
| 3024 |
+
});
|
| 3025 |
+
toolbarHtml += '</select>';
|
| 3026 |
+
toolbarHtml += '</div>';
|
| 3027 |
+
|
| 3028 |
+
var filteredItems = items.filter(function(s) {
|
| 3029 |
+
if (!folderFilter) return true;
|
| 3030 |
+
var folder = s.project ? String(s.project).split(/[\\/]/).pop() : '';
|
| 3031 |
+
return folder === folderFilter;
|
| 3032 |
+
});
|
| 3033 |
+
|
| 3034 |
+
var html = toolbarHtml + '<div class="session-list">';
|
| 3035 |
+
if (filteredItems.length === 0) {
|
| 3036 |
html += '<div class="empty-state"><div class="empty-icon">🗒</div><p>No sessions</p></div>';
|
| 3037 |
} else {
|
| 3038 |
+
filteredItems.forEach(function(s) {
|
| 3039 |
var statusBadge = s.status === 'active' ? 'badge-green' : s.status === 'completed' ? 'badge-blue' : 'badge-muted';
|
| 3040 |
var id = sessionId(s);
|
| 3041 |
var selected = id && state.sessions.selectedId === id;
|
| 3042 |
html += '<div class="session-item' + (selected ? ' selected' : '') + '"' + (id ? ' data-action="select-session" data-session-id="' + esc(id) + '"' : '') + '>';
|
| 3043 |
html += '<div class="session-top"><span class="session-project">' + esc(sessionDisplayName(s)) + '</span>';
|
| 3044 |
html += '<span class="badge ' + statusBadge + '">' + esc(s.status) + '</span></div>';
|
| 3045 |
+
var preview = s.summary || s.firstPrompt || '';
|
| 3046 |
if (preview) {
|
| 3047 |
html += '<div class="session-preview" style="font-size:13px;color:var(--ink);margin:4px 0;line-height:1.4;">' + esc(truncate(preview, 140)) + '</div>';
|
| 3048 |
}
|
|
|
|
| 3056 |
html += '<div id="session-detail"></div>';
|
| 3057 |
el.innerHTML = html;
|
| 3058 |
|
| 3059 |
+
var selectFilter = document.getElementById('sessions-folder-filter');
|
| 3060 |
+
if (selectFilter) {
|
| 3061 |
+
selectFilter.addEventListener('change', function() {
|
| 3062 |
+
state.sessions.folderFilter = this.value;
|
| 3063 |
+
renderSessions();
|
| 3064 |
+
});
|
| 3065 |
+
}
|
| 3066 |
+
|
| 3067 |
if (state.sessions.selectedId) renderSessionDetail();
|
| 3068 |
}
|
| 3069 |
|
|
|
|
| 3102 |
var durationMs = s.endedAt ? new Date(s.endedAt).getTime() - new Date(s.startedAt).getTime() : 0;
|
| 3103 |
var durationLabel = durationMs > 0 ? (durationMs < 60000 ? (durationMs / 1000).toFixed(1) + 's' : (durationMs / 60000).toFixed(1) + 'm') : '-';
|
| 3104 |
|
| 3105 |
+
var preview = s.summary || s.firstPrompt || firstPromptFromObs || '';
|
| 3106 |
|
| 3107 |
var html = '<div class="detail-panel">';
|
| 3108 |
html += '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px;">';
|