feat: Claude Agent integration + cookbook reconnect + UI polish

- Claude Agent integration: AGENT_CONFIGS.claude, INTG_TYPES.claude,
  setup_claude_routes + integrations/claude/ skill bundle. Wired in
  app.py alongside the existing Codex integration; same scope-gated
  /api/codex/* backend; agent form has new description so users know
  it's setup for an external CLI, not an agent streamed inside Odysseus.
- Remove mark_email_boundaries action: not good enough yet. Stripped
  from task UI, scheduler defaults, registry, tool schema, clear-cache
  route. Added to RETIRED_HOUSEKEEPING_ACTIONS so existing rows + their
  task_runs auto-purge on startup.
- Cookbook download reliability: "Reconnect" fix button in the crash
  diagnosis runs _reconnectTask after probing has-session. 30s confirm
  window before marking a download "done" — kills the Finished/Downloading
  flicker when tmux briefly drops between captures.
- Mobile UX: tap anywhere on a note card body opens the editor;
  Update button morphs to Archive when no text was edited; bell icon
  accent-colored; chip-trashing notif pills fade so only the icon
  rotates into the trash zone.
- Settings integrations: SVG-per-provider in email + API preset
  dropdowns, custom drop-up-aware menus, accent sub-header icons
  (IMAP/SMTP), consistent card styling between list + edit, contacts
  Edit/Delete icons, agent form description copy.
This commit is contained in:
pewdiepie-archdaemon
2026-06-04 08:27:26 +09:00
parent 6e80d0de08
commit 089246614d
17 changed files with 1301 additions and 387 deletions
+97 -17
View File
@@ -2456,6 +2456,26 @@ async function _reconnectTask(el, task) {
const isNetwork = /connection|timeout|timed out|incompleteread|chunkedencoding|reset by peer|protocolerror|all connection attempts failed/i.test(lastOutput);
const progressMatch = String(lastOutput || '').match(/(\d+)%\|/);
const nearDone = progressMatch && Number(progressMatch[1]) >= 80;
// Reconnect: most "crashed" downloads near the end are actually
// finished — we just missed the DOWNLOAD_OK / /snapshots/ marker
// because output rolled over, or the tmux session ended a tick
// before we polled. Probing has-session and re-attaching to
// capture-pane lets the existing _reconnectTask flow pick up
// the real state (running, finished, or truly dead).
const _reconnectFix = {
label: 'Reconnect',
action: () => {
_updateTask(task.sessionId, { status: 'running' });
el.dataset.status = 'running';
const badge2 = el.querySelector('.cookbook-task-status');
if (badge2) { badge2.textContent = _statusLabel('running', task.type); badge2.className = 'cookbook-task-status'; }
const _diagEl = el.querySelector('.cookbook-diagnosis');
if (_diagEl) _diagEl.remove();
const _wave = el.querySelector('.cookbook-task-wave'); if (_wave) _wave.style.display = '';
const _up = el.querySelector('.cookbook-task-uptime'); if (_up) _up.style.display = '';
_reconnectTask(el, task);
},
};
const diag = {
message: isDisk
? 'Download stopped because this server ran out of disk space.'
@@ -2467,28 +2487,88 @@ async function _reconnectTask(el, task) {
suggestion: isDisk
? 'Suggested action: free disk space, then retry the download. HuggingFace resumes incomplete files when possible.'
: nearDone
? 'Suggested action: retry the download. It may briefly look like it restarted while cached files are checked, then it should reuse incomplete files.'
: 'Suggested action: retry the download. HuggingFace resumes incomplete files when possible.',
fixes: [
{ label: 'Retry download', action: () => _retryTask(el, task) },
{ label: 'Copy last 50 lines', action: () => {
const last = String(lastOutput || '').split('\n').slice(-50).join('\n');
_copyText(last || 'No download log available.');
} },
],
? 'Suggested action: hit Reconnect first — the download may have finished after the output buffer rolled over. Retry only if reconnect cannot recover.'
: 'Suggested action: hit Reconnect to re-attach to the tmux session. If that fails, retry — HuggingFace resumes incomplete files when possible.',
fixes: isDisk
? [
{ label: 'Retry download', action: () => _retryTask(el, task) },
{ label: 'Copy last 50 lines', action: () => {
const last = String(lastOutput || '').split('\n').slice(-50).join('\n');
_copyText(last || 'No download log available.');
} },
]
: [
_reconnectFix,
{ label: 'Retry download', action: () => _retryTask(el, task) },
{ label: 'Copy last 50 lines', action: () => {
const last = String(lastOutput || '').split('\n').slice(-50).join('\n');
_copyText(last || 'No download log available.');
} },
],
};
_showDiagnosis(el, diag, lastOutput);
// Auto-probe: if the tmux session is still alive (download
// genuinely still in progress), _selfHealStaleTasks flips the
// task back to running and the diagnosis disappears without
// the user needing to click Reconnect.
if (nearDone) setTimeout(() => { _selfHealStaleTasks().catch(() => {}); }, 1200);
}
_showCookbookNotif(true);
} else {
_updateTask(task.sessionId, { status: 'done' });
el.dataset.status = 'done';
const badge = el.querySelector('.cookbook-task-status');
if (badge) { badge.textContent = _statusLabel('done', task.type); badge.className = 'cookbook-task-status cookbook-task-done'; }
const _chk = el.querySelector('.cookbook-task-check'); if (_chk) _chk.style.display = '';
const _sb = el.querySelector('.cookbook-task-serve-btn'); if (_sb) _sb.style.display = '';
_showCookbookNotif();
_refreshDepsAfterInstall(task);
// Debounce the done flip. Tmux capture-pane can fail transiently
// (network blip, ssh reconnect), and the verify has-session right
// above can briefly report dead even when the session is in the
// middle of finalizing. Marking done immediately + the periodic
// _selfHealStaleTasks then flipping back to running causes the
// status badge to oscillate between Finished and Downloading.
// Wait 30s and re-probe: only finalize as done if tmux is STILL
// gone. If the session resurfaces, restart _reconnectTask so live
// capture resumes without the user seeing a fake "done" first.
if (!task._doneConfirmAt) {
_updateTask(task.sessionId, { _doneConfirmAt: Date.now() + 30000 });
setTimeout(async () => {
try {
const fresh = _loadTasks().find(t => t.sessionId === task.sessionId);
if (!fresh) return;
let stillAlive = false;
try {
const probe = await fetch('/api/shell/exec', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command: _tmuxCmd(task, `has-session -t ${task.sessionId}`), timeout: 5 }),
});
const pData = await probe.json();
stillAlive = pData.exit_code === 0;
} catch { /* network blip — treat as inconclusive, prefer running */ stillAlive = true; }
if (stillAlive) {
_updateTask(task.sessionId, { status: 'running', _doneConfirmAt: null });
const _el = document.querySelector(`.cookbook-task[data-task-id="${task.sessionId}"]`);
if (_el) {
_el.dataset.status = 'running';
const _badge = _el.querySelector('.cookbook-task-status');
if (_badge) { _badge.textContent = _statusLabel('running', task.type); _badge.className = 'cookbook-task-status'; }
const _wave = _el.querySelector('.cookbook-task-wave'); if (_wave) _wave.style.display = '';
const _up = _el.querySelector('.cookbook-task-uptime'); if (_up) _up.style.display = '';
_reconnectTask(_el, _loadTasks().find(t => t.sessionId === task.sessionId));
}
return;
}
_updateTask(task.sessionId, { status: 'done', _doneConfirmAt: null });
const _el = document.querySelector(`.cookbook-task[data-task-id="${task.sessionId}"]`);
if (_el) {
_el.dataset.status = 'done';
const _badge = _el.querySelector('.cookbook-task-status');
if (_badge) { _badge.textContent = _statusLabel('done', task.type); _badge.className = 'cookbook-task-status cookbook-task-done'; }
const _chk = _el.querySelector('.cookbook-task-check'); if (_chk) _chk.style.display = '';
const _sb = _el.querySelector('.cookbook-task-serve-btn'); if (_sb) _sb.style.display = '';
}
_showCookbookNotif();
_refreshDepsAfterInstall(task);
_renderRunningTab();
_processQueue();
} catch { /* swallow — next polling cycle will retry */ }
}, 30000);
}
}
}
_renderRunningTab();