mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-30 00:22:10 -04:00
Notes: checklist/todo/goal classification + agent-stream-complete state class for done indicator
This commit is contained in:
+157
-7
@@ -590,7 +590,7 @@ function _isNoteFullyDone(note) {
|
|||||||
// A "checklist note" — todo or goal — has structured items[] that the cards
|
// A "checklist note" — todo or goal — has structured items[] that the cards
|
||||||
// render as checkboxes and that "fully done" / progress logic reads from.
|
// render as checkboxes and that "fully done" / progress logic reads from.
|
||||||
function _hasItems(note) {
|
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
|
// Compact " N/M" progress string for a goal's checklist. Empty when the goal
|
||||||
@@ -1099,9 +1099,6 @@ export function openPanel() {
|
|||||||
if (_open) return;
|
if (_open) return;
|
||||||
_open = true;
|
_open = true;
|
||||||
_editingId = null;
|
_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();
|
_clearViewedReminderGlows();
|
||||||
_firedDotDismissedAt = Date.now();
|
_firedDotDismissedAt = Date.now();
|
||||||
try { localStorage.setItem(REMINDER_DISMISSED_AT_KEY, String(_firedDotDismissedAt)); } catch {}
|
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++) {
|
for (let i = 0; i < note.items.length; i++) {
|
||||||
const item = note.items[i];
|
const item = note.items[i];
|
||||||
const doneClass = item.done ? ' done' : '';
|
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);
|
const indent = Math.min(item.indent || 0, 3);
|
||||||
contentHtml += `<div class="note-checkbox${doneClass}" data-note-id="${note.id}" data-idx="${i}" style="padding-left:${indent * 16}px">
|
contentHtml += `<div class="note-checkbox${doneClass}" data-note-id="${note.id}" data-idx="${i}" style="padding-left:${indent * 16}px">
|
||||||
<span class="note-check-dot" title="Mark done"></span>
|
<span class="note-check-dot" title="Mark done"></span>
|
||||||
<span class="note-check-text">${_linkify(item.text)}</span>
|
<span class="note-check-text">${_linkify(item.text)}</span>
|
||||||
|
<button class="note-checkbox-agent${agentDoneClass}" data-note-id="${note.id}" data-idx="${i}"${agentSessionAttr} data-agent-title="${_attrEsc(agentMenuTitle)}" title="${agentTitle}">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect x="4" y="8" width="16" height="12" rx="2"/><path d="M2 14h2M20 14h2M15 13v2M9 13v2"/></svg>
|
||||||
|
</button>
|
||||||
<button class="note-checkbox-rm" data-note-id="${note.id}" data-idx="${i}" title="Delete item">
|
<button class="note-checkbox-rm" data-note-id="${note.id}" data-idx="${i}" title="Delete item">
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -2152,7 +2159,7 @@ function _bindCardEvents(body) {
|
|||||||
// Click empty area of checklist preview (not on checkbox/X) — edit
|
// Click empty area of checklist preview (not on checkbox/X) — edit
|
||||||
body.querySelectorAll('.note-checklist-preview').forEach(el => {
|
body.querySelectorAll('.note-checklist-preview').forEach(el => {
|
||||||
el.addEventListener('click', (e) => {
|
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();
|
e.stopPropagation();
|
||||||
tapToEditOrSelect(el.closest('.note-card'));
|
tapToEditOrSelect(el.closest('.note-card'));
|
||||||
});
|
});
|
||||||
@@ -2178,7 +2185,7 @@ function _bindCardEvents(body) {
|
|||||||
// title / content preview triggered edit, so padding + empty gutters were
|
// title / content preview triggered edit, so padding + empty gutters were
|
||||||
// dead zones that felt broken on mobile.
|
// dead zones that felt broken on mobile.
|
||||||
if (_isNotesMobileMode() && !_selectMode) {
|
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 => {
|
body.querySelectorAll('.note-card').forEach(card => {
|
||||||
card.addEventListener('click', (e) => {
|
card.addEventListener('click', (e) => {
|
||||||
if (e.target.closest(_INTERACTIVE)) return;
|
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)
|
// Quick-add new checklist item (hover input at bottom of todo cards)
|
||||||
body.querySelectorAll('.note-cl-quickadd-input').forEach(input => {
|
body.querySelectorAll('.note-cl-quickadd-input').forEach(input => {
|
||||||
input.addEventListener('click', (e) => e.stopPropagation());
|
input.addEventListener('click', (e) => e.stopPropagation());
|
||||||
@@ -4317,6 +4336,56 @@ function _openNoteCornerMenu(btn) {
|
|||||||
menu.querySelector('[data-act="agent"]').addEventListener('click', () => { menu.remove(); _agentSolveNote(id); });
|
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 = `
|
||||||
|
<div class="ncm-title">${_esc(title)}</div>
|
||||||
|
${sid ? `<button type="button" class="ncm-item" data-act="open">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14L21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
|
||||||
|
<span>Open this agent chat</span>
|
||||||
|
</button>` : ''}
|
||||||
|
<button type="button" class="ncm-item" data-act="run">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect x="4" y="8" width="16" height="12" rx="2"/><path d="M2 14h2M20 14h2M15 13v2M9 13v2"/></svg>
|
||||||
|
<span>${sid ? 'Run again for this todo' : 'Start agent for this todo'}</span>
|
||||||
|
</button>`;
|
||||||
|
_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
|
// Build the prompt the agent gets from a note: title + body, plus any
|
||||||
// not-yet-done checklist items.
|
// not-yet-done checklist items.
|
||||||
function _noteToAgentPrompt(note) {
|
function _noteToAgentPrompt(note) {
|
||||||
@@ -4328,7 +4397,7 @@ function _noteToAgentPrompt(note) {
|
|||||||
.forEach(it => parts.push('- ' + it.text.trim()));
|
.forEach(it => parts.push('- ' + it.text.trim()));
|
||||||
}
|
}
|
||||||
const body = parts.join('\n');
|
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
|
// 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('message', prompt);
|
||||||
fd.append('session', sid);
|
fd.append('session', sid);
|
||||||
fd.append('mode', 'agent');
|
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 })
|
fetch(`${API_BASE}/api/chat_stream`, { method: 'POST', credentials: 'same-origin', body: fd })
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (!res.ok || !res.body) return;
|
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) {
|
async function _copyNote(noteId, btnEl) {
|
||||||
const note = _notes.find(n => n.id === noteId);
|
const note = _notes.find(n => n.id === noteId);
|
||||||
if (!note) return false;
|
if (!note) return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user