personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

home: add Daily Goal and Upcoming cards; introduce Inbox app

Enhance the Home dashboard with facet-aware Daily Goal (from pending todos) and a rolling 3-day Upcoming view of events and todos. Introduce a top-level Inbox app with background service support for error/alert tracking.

+243 -1
+32
apps/home/routes.py
··· 164 164 or facet_data["entities"] 165 165 ) 166 166 167 + # Extract daily goal (first pending todo) 168 + facet_data["goal"] = next((t["text"] for t in facet_data["todos"] if not t["completed"]), None) 169 + 167 170 # Add facet metadata 168 171 result["facet_meta"][facet_name] = { 169 172 "title": facet_config.get("title", facet_name.title()), ··· 174 177 175 178 # Add aggregated events by agent to totals 176 179 result["totals"]["events_by_agent"] = dict(events_by_agent) 180 + 181 + # Load upcoming items (next 3 days) 182 + upcoming_data = [] 183 + try: 184 + ref_date = datetime.strptime(day, "%Y%m%d") 185 + for i in range(1, 4): 186 + next_day = (ref_date + timedelta(days=i)).strftime("%Y%m%d") 187 + day_events = get_events(next_day) 188 + for e in day_events: 189 + upcoming_data.append({ 190 + "type": "event", 191 + "day": next_day, 192 + "title": e.get("title") or e.get("summary", "Untitled Event"), 193 + "facet": e.get("facet") 194 + }) 195 + for f_name in facet_names: 196 + f_todos = get_todos(next_day, f_name) 197 + if f_todos: 198 + for t in f_todos: 199 + if not t.get("completed") and not t.get("cancelled"): 200 + upcoming_data.append({ 201 + "type": "todo", 202 + "day": next_day, 203 + "title": t.get("text"), 204 + "facet": f_name 205 + }) 206 + except Exception: 207 + pass 208 + result["upcoming"] = upcoming_data[:5] # Limit to 5 items 177 209 178 210 # Load recent entities (attached entities with last_seen in lookback window) 179 211 recent_entities = _get_recent_entities(day, facet_names)
+123 -1
apps/home/workspace.html
··· 19 19 </div> 20 20 21 21 <div class="home-dashboard" id="home-dashboard" style="display: none;"> 22 - <!-- Top row: Todos and Events --> 22 + <!-- Top row: Goal and Upcoming --> 23 + <div class="home-grid"> 24 + <div class="home-card" id="goal-card"> 25 + <h2 class="home-card-title">Daily Goal</h2> 26 + <div class="home-card-content" id="goal-content"> 27 + <div class="home-empty">No goal set</div> 28 + </div> 29 + </div> 30 + 31 + <div class="home-card" id="upcoming-card"> 32 + <h2 class="home-card-title">Upcoming</h2> 33 + <div class="home-card-content" id="upcoming-content"> 34 + <div class="home-empty">Nothing upcoming</div> 35 + </div> 36 + </div> 37 + </div> 38 + 39 + <!-- Middle row: Todos and Events --> 23 40 <div class="home-grid"> 24 41 <div class="home-card" id="todos-card"> 25 42 <h2 class="home-card-title">Todos</h2> ··· 301 318 font-size: 0.85rem; 302 319 } 303 320 321 + /* Goal and Upcoming styles */ 322 + .home-goal-text { 323 + font-size: 1.1rem; 324 + font-weight: 500; 325 + color: #111827; 326 + } 327 + 328 + .home-upcoming-list { 329 + list-style: none; 330 + padding: 0; 331 + margin: 0; 332 + } 333 + 334 + .home-upcoming-item { 335 + display: flex; 336 + gap: 0.75rem; 337 + padding: 0.2rem 0; 338 + font-size: 0.9rem; 339 + border-bottom: 1px solid #f3f4f6; 340 + } 341 + 342 + .home-upcoming-item:last-child { 343 + border-bottom: none; 344 + } 345 + 346 + .home-upcoming-day { 347 + color: #6b7280; 348 + font-weight: 600; 349 + min-width: 3.5rem; 350 + flex-shrink: 0; 351 + } 352 + 353 + .home-upcoming-title { 354 + color: #374151; 355 + overflow: hidden; 356 + text-overflow: ellipsis; 357 + white-space: nowrap; 358 + } 359 + 304 360 /* News content - markdown typography */ 305 361 .home-news-content { 306 362 color: #374151; ··· 359 415 return currentDay === today || currentDay === yesterday; 360 416 } 361 417 418 + function renderGoal(data, facet) { 419 + const container = document.getElementById('goal-content'); 420 + let goal = null; 421 + 422 + if (facet) { 423 + goal = data.facets[facet]?.goal; 424 + } else { 425 + // Find first goal across facets 426 + for (const facetName of Object.keys(data.facets)) { 427 + if (data.facets[facetName].goal) { 428 + goal = data.facets[facetName].goal; 429 + break; 430 + } 431 + } 432 + } 433 + 434 + if (!goal) { 435 + container.innerHTML = '<div class="home-empty">No goal set</div>'; 436 + return; 437 + } 438 + 439 + container.innerHTML = ` 440 + <div class="home-goal-text">${escapeHtml(goal)}</div> 441 + `; 442 + } 443 + 444 + function renderUpcoming(data) { 445 + const container = document.getElementById('upcoming-content'); 446 + const upcoming = data.upcoming || []; 447 + 448 + if (upcoming.length === 0) { 449 + container.innerHTML = '<div class="home-empty">Nothing upcoming</div>'; 450 + return; 451 + } 452 + 453 + container.innerHTML = ` 454 + <ul class="home-upcoming-list"> 455 + ${upcoming.map(item => ` 456 + <li class="home-upcoming-item"> 457 + <span class="home-upcoming-day">${formatDay(item.day)}</span> 458 + <span class="home-upcoming-title" title="${escapeHtml(item.title)}">${escapeHtml(item.title)}</span> 459 + </li> 460 + `).join('')} 461 + </ul> 462 + `; 463 + } 464 + 465 + function formatDay(dayStr) { 466 + if (!dayStr) return ''; 467 + const today = new Date().toISOString().slice(0, 10).replace(/-/g, ''); 468 + if (dayStr === today) return 'Today'; 469 + 470 + const tomorrowDate = new Date(); 471 + tomorrowDate.setDate(tomorrowDate.getDate() + 1); 472 + const tomorrow = tomorrowDate.toISOString().slice(0, 10).replace(/-/g, ''); 473 + if (dayStr === tomorrow) return 'Tomorrow'; 474 + 475 + // Return e.g. "Wed" 476 + const year = parseInt(dayStr.substring(0, 4)); 477 + const month = parseInt(dayStr.substring(4, 6)) - 1; 478 + const day = parseInt(dayStr.substring(6, 8)); 479 + return new Date(year, month, day).toLocaleDateString(undefined, { weekday: 'short' }); 480 + } 481 + 362 482 function renderTodos(data, facet) { 363 483 const container = document.getElementById('todos-content'); 364 484 let todos = []; ··· 609 729 } 610 730 611 731 function renderSingleFacetView(data, facet) { 732 + renderGoal(data, facet); 733 + renderUpcoming(data); 612 734 renderTodos(data, facet); 613 735 renderEvents(data, facet); 614 736 renderEntities(data, facet);
+7
apps/inbox/app.json
··· 1 + { 2 + "icon": "📥", 3 + "label": "Inbox", 4 + "facets": { 5 + "disabled": true 6 + } 7 + }
+34
apps/inbox/background.html
··· 1 + {# Inbox app background service - manages badge and notifications for items requiring attention #} 2 + 3 + AppServices.register('inbox', { 4 + count: 0, 5 + 6 + initialize() { 7 + // Listen for cortex events to track items that might need attention 8 + appEvents.listen('cortex', (e) => this.handleCortexEvent(e)); 9 + }, 10 + 11 + handleCortexEvent(e) { 12 + // We only care about completions and errors 13 + if (e.event !== 'finish' && e.event !== 'error') return; 14 + 15 + // Increment inbox count for errors 16 + if (e.event === 'error') { 17 + this.count++; 18 + this.updateBadge(); 19 + } 20 + }, 21 + 22 + updateBadge() { 23 + if (this.count > 0) { 24 + AppServices.badges.app.set('inbox', this.count); 25 + } else { 26 + AppServices.badges.app.clear('inbox'); 27 + } 28 + }, 29 + 30 + clear() { 31 + this.count = 0; 32 + this.updateBadge(); 33 + } 34 + });
+42
apps/inbox/workspace.html
··· 1 + <div class="workspace-content"> 2 + <div class="inbox-container"> 3 + <div class="inbox-empty"> 4 + <div class="inbox-empty-icon">📥</div> 5 + <h2>Your Inbox is empty</h2> 6 + <p>This is where your notifications, alerts, and items requiring attention will appear.</p> 7 + </div> 8 + </div> 9 + </div> 10 + 11 + <style> 12 + .inbox-container { 13 + display: flex; 14 + flex-direction: column; 15 + gap: 1.5rem; 16 + max-width: 800px; 17 + margin: 0 auto; 18 + } 19 + 20 + .inbox-empty { 21 + text-align: center; 22 + padding: 5rem 2rem; 23 + color: #6b7280; 24 + } 25 + 26 + .inbox-empty-icon { 27 + font-size: 4rem; 28 + margin-bottom: 1rem; 29 + opacity: 0.5; 30 + } 31 + 32 + .inbox-empty h2 { 33 + margin: 0 0 0.5rem 0; 34 + color: #111827; 35 + font-size: 1.5rem; 36 + } 37 + 38 + .inbox-empty p { 39 + margin: 0; 40 + font-size: 1.1rem; 41 + } 42 + </style>
+5
convey/templates/menu_bar.html
··· 1 1 <!-- Menu Bar Component (left sidebar) --> 2 2 <div class="menu-bar"> 3 3 <div class="menu-items"> 4 + <a href="/app/inbox" class="menu-item{% if app == 'inbox' %} current{% endif %}" data-app-name="inbox" data-starred="true"> 5 + <span class="icon">📥</span> 6 + <span class="label">Inbox</span> 7 + </a> 8 + 4 9 {%- set app_names = apps.keys() | list -%} 5 10 {%- set last_starred_index = -1 -%} 6 11 {%- for i in range(app_names | length) -%}