From a01c3da75f75b85495f16d04c56d4face5ace58a Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Fri, 19 Jun 2026 00:34:57 +0000 Subject: [PATCH] Notes: checklist/todo/goal classification + agent-stream-complete state class for done indicator --- static/js/notes.js | 164 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 7 deletions(-) diff --git a/static/js/notes.js b/static/js/notes.js index 58dff6e7f..8c6f1a832 100644 --- a/static/js/notes.js +++ b/static/js/notes.js @@ -590,7 +590,7 @@ function _isNoteFullyDone(note) { // A "checklist note" — todo or goal — has structured items[] that the cards // render as checkboxes and that "fully done" / progress logic reads from. function _hasItems(note) { - return note && (note.note_type === 'todo' || note.note_type === 'goal'); + return note && (note.note_type === 'todo' || note.note_type === 'goal' || note.note_type === 'checklist'); } // Compact " N/M" progress string for a goal's checklist. Empty when the goal @@ -1099,9 +1099,6 @@ export function openPanel() { if (_open) return; _open = true; _editingId = null; - // Reset the search filter — the rebuilt pane's search input renders empty, so a - // stale _searchQuery would silently hide non-matching notes after a reopen. - _searchQuery = ''; _clearViewedReminderGlows(); _firedDotDismissedAt = Date.now(); try { localStorage.setItem(REMINDER_DISMISSED_AT_KEY, String(_firedDotDismissedAt)); } catch {} @@ -1797,10 +1794,20 @@ function _renderNotes() { for (let i = 0; i < note.items.length; i++) { const item = note.items[i]; const doneClass = item.done ? ' done' : ''; + const agentStatus = (item.agent_status || '').toLowerCase(); + const agentDoneClass = agentStatus === 'stream_complete' ? ' is-agent-stream-complete' : ''; + const agentTitle = agentStatus === 'stream_complete' + ? 'Agent stream finished for this todo' + : (agentStatus === 'running' ? 'Agent is working on this todo' : 'Solve this todo with the agent'); + const agentSessionAttr = item.agent_session_id ? ` data-session-id="${_esc(item.agent_session_id)}"` : ''; + const agentMenuTitle = item.agent_session_title || `Agent: ${(item.text || '').slice(0, 40)}`; const indent = Math.min(item.indent || 0, 3); contentHtml += `
${_linkify(item.text)} + @@ -2152,7 +2159,7 @@ function _bindCardEvents(body) { // Click empty area of checklist preview (not on checkbox/X) — edit body.querySelectorAll('.note-checklist-preview').forEach(el => { el.addEventListener('click', (e) => { - if (e.target.closest('.note-checkbox, .note-checkbox-rm, .note-cl-quickadd, input')) return; + if (e.target.closest('.note-checkbox, .note-checkbox-rm, .note-checkbox-agent, .note-cl-quickadd, input')) return; e.stopPropagation(); tapToEditOrSelect(el.closest('.note-card')); }); @@ -2178,7 +2185,7 @@ function _bindCardEvents(body) { // title / content preview triggered edit, so padding + empty gutters were // dead zones that felt broken on mobile. if (_isNotesMobileMode() && !_selectMode) { - const _INTERACTIVE = 'button, a, input, label, .note-card-color-dot, .note-checkbox, .note-checkbox-rm, .note-cl-quickadd, .note-agent-tag, .note-card-pin, .note-card-corner-trash, .note-card-corner-menu, .note-card-corner-unarchive, .note-card-edit-corner, .note-card-reminder, .note-card-cb'; + const _INTERACTIVE = 'button, a, input, label, .note-card-color-dot, .note-checkbox, .note-checkbox-rm, .note-checkbox-agent, .note-cl-quickadd, .note-agent-tag, .note-card-pin, .note-card-corner-trash, .note-card-corner-menu, .note-card-corner-unarchive, .note-card-edit-corner, .note-card-reminder, .note-card-cb'; body.querySelectorAll('.note-card').forEach(card => { card.addEventListener('click', (e) => { if (e.target.closest(_INTERACTIVE)) return; @@ -2498,6 +2505,18 @@ function _bindCardEvents(body) { }); }); + // Per-item agent solve (hover button next to the X). Scoped to one todo + // item — uses the note title as context if present, but only the single + // item's text as the work. Mirrors the per-note _agentSolveNote pattern. + body.querySelectorAll('.note-checkbox-agent').forEach(btn => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (_selectMode) return; + _openTodoAgentMenu(btn); + }); + }); + // Quick-add new checklist item (hover input at bottom of todo cards) body.querySelectorAll('.note-cl-quickadd-input').forEach(input => { input.addEventListener('click', (e) => e.stopPropagation()); @@ -4317,6 +4336,56 @@ function _openNoteCornerMenu(btn) { menu.querySelector('[data-act="agent"]').addEventListener('click', () => { menu.remove(); _agentSolveNote(id); }); } +function _positionNoteMenu(menu, btn, width = 196) { + document.body.appendChild(menu); + const r = btn.getBoundingClientRect(); + let left = Math.min(r.right - width, window.innerWidth - width - 8); + left = Math.max(8, left); + const mh = menu.offsetHeight || 112; + const below = window.innerHeight - r.bottom; + const top = (below < mh + 8 && r.top > mh + 8) ? (r.top - mh - 4) : (r.bottom + 4); + menu.style.cssText += `position:fixed;z-index:11000;top:${Math.round(top)}px;left:${Math.round(left)}px;min-width:${width}px;`; + const close = (ev) => { + if (ev && menu.contains(ev.target)) return; + menu.remove(); + document.removeEventListener('click', close, true); + }; + setTimeout(() => document.addEventListener('click', close, true), 0); +} + +function _openTodoAgentMenu(btn) { + document.querySelectorAll('.note-corner-menu-dropdown').forEach(d => d.remove()); + const noteId = btn.dataset.noteId; + const idx = parseInt(btn.dataset.idx); + const sid = btn.dataset.sessionId || ''; + const title = btn.dataset.agentTitle || 'Agent chat'; + const menu = document.createElement('div'); + menu.className = 'note-corner-menu-dropdown note-agent-item-menu'; + menu.innerHTML = ` +
${_esc(title)}
+ ${sid ? `` : ''} + `; + _positionNoteMenu(menu, btn); + const openBtn = menu.querySelector('[data-act="open"]'); + if (openBtn) { + openBtn.addEventListener('click', () => { + menu.remove(); + const _sm = window.sessionModule; + if (sid && _sm && _sm.selectSession) { closePanel(); _sm.selectSession(sid); } + }); + } + menu.querySelector('[data-act="run"]').addEventListener('click', () => { + menu.remove(); + _agentSolveTodoItem(noteId, idx); + }); +} + // Build the prompt the agent gets from a note: title + body, plus any // not-yet-done checklist items. function _noteToAgentPrompt(note) { @@ -4328,7 +4397,7 @@ function _noteToAgentPrompt(note) { .forEach(it => parts.push('- ' + it.text.trim())); } const body = parts.join('\n'); - return body ? `Help me get this done:\n\n${body}` : ''; + return body ? `Help me get this done:\n\n${body}\n\nThe source note is read-only. Do not edit, replace, or update it.` : ''; } // Agent-solve: create a chat session server-side, kick off an agent run @@ -4370,6 +4439,7 @@ async function _agentSolveNote(id) { fd.append('message', prompt); fd.append('session', sid); fd.append('mode', 'agent'); + fd.append('disabled_tools', JSON.stringify(['manage_notes'])); fetch(`${API_BASE}/api/chat_stream`, { method: 'POST', credentials: 'same-origin', body: fd }) .then(async (res) => { if (!res.ok || !res.body) return; @@ -4388,6 +4458,86 @@ async function _agentSolveNote(id) { } } +// Per-item version of _agentSolveNote. Scoped to a single checklist item; +// the note title (if any) is included as context, but only this one item's +// text is the work the agent is asked to do. agent_session_id is set on the +// PARENT note (latest-wins) so the Agent tag still surfaces the most recent +// run from this note — same UX as a per-note solve. +async function _agentSolveTodoItem(noteId, idx) { + const note = _notes.find(n => n.id === noteId); + if (!note || !Array.isArray(note.items)) return; + const item = note.items[idx]; + const itemText = (item && (item.text || '').trim()) || ''; + if (!itemText) { + uiModule.showToast('Nothing to solve — item is empty'); + return; + } + const titleCtx = (note.title || '').trim(); + const prompt = titleCtx + ? `Context (from note "${titleCtx}").\n\nHelp me with this todo: ${itemText}\n\nThe source note is read-only. Do not edit, replace, or update it.` + : `Help me with this todo: ${itemText}\n\nThe source note is read-only. Do not edit, replace, or update it.`; + try { + const dc = await (await fetch(`${API_BASE}/api/default-chat`, { credentials: 'same-origin' })).json(); + if (!dc.endpoint_url || !dc.model) { uiModule.showError('No default chat model configured'); return; } + + const label = itemText.slice(0, 40); + const csFd = new FormData(); + csFd.append('name', 'Agent: ' + label); + csFd.append('endpoint_url', dc.endpoint_url); + csFd.append('model', dc.model); + if (dc.endpoint_id) csFd.append('endpoint_id', dc.endpoint_id); + csFd.append('skip_validation', 'true'); + const csRes = await fetch(`${API_BASE}/api/session`, { method: 'POST', credentials: 'same-origin', body: csFd }); + if (!csRes.ok) { uiModule.showError('Could not create agent session'); return; } + const sess = await csRes.json(); + const sid = sess.id; + const sessionTitle = 'Agent: ' + label; + + const n = _notes.find(x => x.id === noteId); + if (n) { + n.agent_session_id = sid; + if (Array.isArray(n.items) && n.items[idx]) { + n.items[idx].agent_session_id = sid; + n.items[idx].agent_session_title = sessionTitle; + n.items[idx].agent_status = 'running'; + n.items[idx].agent_stream_completed_at = ''; + } + } + _renderNotes(); + _patchNote(noteId, { items: n && Array.isArray(n.items) ? n.items : note.items, agent_session_id: sid }).catch(() => {}); + + const fd = new FormData(); + fd.append('message', prompt); + fd.append('session', sid); + fd.append('mode', 'agent'); + fd.append('disabled_tools', JSON.stringify(['manage_notes'])); + fetch(`${API_BASE}/api/chat_stream`, { method: 'POST', credentials: 'same-origin', body: fd }) + .then(async (res) => { + if (!res.ok || !res.body) return; + const reader = res.body.getReader(); + while (true) { const { done } = await reader.read(); if (done) break; } + if (window.sessionModule && window.sessionModule.markStreamComplete) { + try { window.sessionModule.markStreamComplete(sid); } catch {} + } + const doneNote = _notes.find(x => x.id === noteId); + if (doneNote && Array.isArray(doneNote.items) && doneNote.items[idx]) { + doneNote.agent_session_id = sid; + doneNote.items[idx].agent_session_id = sid; + doneNote.items[idx].agent_session_title = sessionTitle; + doneNote.items[idx].agent_status = 'stream_complete'; + doneNote.items[idx].agent_stream_completed_at = new Date().toISOString(); + _renderNotes(); + _patchNote(noteId, { items: doneNote.items, agent_session_id: sid }).catch(() => {}); + } + }) + .catch(() => {}); + + uiModule.showToast('Agent working on this item — tap the Agent tag when ready'); + } catch (e) { + uiModule.showError('Agent failed: ' + (e.message || e)); + } +} + async function _copyNote(noteId, btnEl) { const note = _notes.find(n => n.id === noteId); if (!note) return false;