1 Commits

Author SHA1 Message Date
pewdiepie-archdaemon f552269718 feat(notes): per-item Agent: solve button on todo hover
Adds a hover-revealed agent button next to each todo item's X button,
mirroring the existing per-note "Agent: solve this" affordance but
scoped to a single checklist item. Clicking it creates a new chat
session named "Agent: {item text}", kicks off the agent run in the
background (user stays in notes), and links the latest run to the
parent note via agent_session_id so the existing Agent tag opens the
chat — same review path as the per-note solve.

The prompt is "Help me with this todo: {item text}" plus the note
title as one-line context when present, so the agent has enough
framing without being handed the whole note.

Why per-item: granular delegation is the main user-facing missing
piece. Today you can hand a whole note to the agent, but if the note
has 5 todos and only one needs agent attention, you either solve all
five or none. Per-item closes that gap without changing the existing
per-note flow.

UI:
- New .note-checkbox-agent button in the item template, placed before
  the X so DOM order is text → agent → X.
- Reuses the existing robot SVG (same icon as the corner-menu Agent
  item) at 10x10 to match the X visually.
- CSS mirrors .note-checkbox-rm exactly: hidden until item hover,
  opacity 0.55 on item hover, opacity 1 + accent on button hover.
- Hidden in select mode and in the same _INTERACTIVE/skip-click lists
  as the X so card clicks don't fire.

Backend: none — uses existing /api/session + /api/chat_stream agent
mode. No DB schema change. agent_session_id is latest-wins on the
parent note (matches existing per-note behavior).

Targets `dev`. Visual change — needs a browser test before promoting
to main.
2026-06-05 12:01:41 +09:00
2 changed files with 102 additions and 2 deletions
+78 -2
View File
@@ -1783,6 +1783,9 @@ function _renderNotes() {
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" data-note-id="${note.id}" data-idx="${i}" title="Solve this todo with the agent">
<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>
@@ -2134,7 +2137,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'));
}); });
@@ -2160,7 +2163,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;
@@ -2480,6 +2483,19 @@ 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.stopPropagation();
if (_selectMode) return;
const noteId = btn.dataset.noteId;
const idx = parseInt(btn.dataset.idx);
_agentSolveTodoItem(noteId, idx);
});
});
// 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());
@@ -4370,6 +4386,66 @@ 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}`
: `Help me with this todo: ${itemText}`;
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 n = _notes.find(x => x.id === noteId);
if (n) n.agent_session_id = sid;
_renderNotes();
_patchNote(noteId, { agent_session_id: sid }).catch(() => {});
const fd = new FormData();
fd.append('message', prompt);
fd.append('session', sid);
fd.append('mode', 'agent');
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 {}
}
})
.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;
+24
View File
@@ -32126,6 +32126,30 @@ body.notes-mobile-mode.notes-drag-mode .note-card-pin.active {
.note-checkbox:hover .note-checkbox-rm { opacity: 0.55; } .note-checkbox:hover .note-checkbox-rm { opacity: 0.55; }
.note-checkbox-rm:hover { opacity: 1 !important; color: var(--red); background: color-mix(in srgb, var(--red) 12%, transparent); } .note-checkbox-rm:hover { opacity: 1 !important; color: var(--red); background: color-mix(in srgb, var(--red) 12%, transparent); }
.note-card-selectmode .note-checkbox-rm { display: none; } .note-card-selectmode .note-checkbox-rm { display: none; }
/* Per-item "solve with agent" button mirrors .note-checkbox-rm visually
so the two hover-actions on a todo item read as a pair. Sits to the left
of the X (DOM order = agent then X), both pushed to the right via the
shared margin-left:auto pattern. */
.note-checkbox-agent {
flex: 0 0 auto;
background: transparent;
border: none;
color: var(--fg);
opacity: 0;
cursor: pointer;
padding: 2px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
margin-right: 0;
transition: opacity 0.12s, background 0.12s, color 0.12s;
}
.note-checkbox:hover .note-checkbox-agent { opacity: 0.55; }
.note-checkbox-agent:hover { opacity: 1 !important; color: var(--red); background: color-mix(in srgb, var(--red) 12%, transparent); }
.note-card-selectmode .note-checkbox-agent { display: none; }
.note-check-dot { .note-check-dot {
width: 16px; width: 16px;
height: 16px; height: 16px;