mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user