mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-28 07:35:27 -04:00
Cookbook task menu: merge Edit actions + group items into sections
- Removed standalone "Edit cmd & relaunch" — "Edit in serve panel" renamed to "Edit & relaunch" and is now the single edit entry. Tooltip notes that the raw cmd is still editable inside the panel. - Tagged each item with a group (run / edit / endpoint / copy / danger) and renderer inserts a thin divider whenever the group changes, so the menu reads as visual blocks instead of one long list.
This commit is contained in:
@@ -2099,57 +2099,43 @@ export function _renderRunningTab() {
|
|||||||
dropdown.className = 'cookbook-task-dropdown';
|
dropdown.className = 'cookbook-task-dropdown';
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
|
// ── Run section ─────────────────────────────────────────────
|
||||||
// Queued download: let the user jump the queue and start it immediately
|
// Queued download: let the user jump the queue and start it immediately
|
||||||
// (downloads otherwise run one-at-a-time per server).
|
// (downloads otherwise run one-at-a-time per server).
|
||||||
if (task.type === 'download' && task.status === 'queued') {
|
if (task.type === 'download' && task.status === 'queued') {
|
||||||
items.push({ label: 'Start now', action: 'start-now', custom: () => {
|
items.push({ group: 'run', label: 'Start now', action: 'start-now', custom: () => {
|
||||||
_startQueuedDownload(task);
|
_startQueuedDownload(task);
|
||||||
_renderRunningTab();
|
_renderRunningTab();
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
if (task.status !== 'running' && task.status !== 'queued') {
|
if (task.status !== 'running' && task.status !== 'queued') {
|
||||||
items.push({ label: 'Reconnect tmux', action: 'reconnect' });
|
items.push({ group: 'run', label: 'Reconnect tmux', action: 'reconnect' });
|
||||||
}
|
}
|
||||||
if (task.status === 'running') {
|
if (task.status === 'running') {
|
||||||
items.push({ label: 'Stop', action: 'stop', danger: true });
|
items.push({ group: 'run', label: 'Stop', action: 'stop', danger: true });
|
||||||
}
|
}
|
||||||
items.push({ label: 'Restart', action: 'retry' });
|
items.push({ group: 'run', label: 'Restart', action: 'retry' });
|
||||||
// Edit serve — open the full serve panel (same as the edit icon),
|
// ── Edit section ────────────────────────────────────────────
|
||||||
// switching to this task's server first so the model is found.
|
// Merged "Edit & relaunch" — opens the structured serve panel
|
||||||
|
// pre-filled with this task's config. The old standalone "Edit
|
||||||
|
// cmd & relaunch" raw-text dialog is now reachable from inside
|
||||||
|
// that panel (Show command). Single entry-point per task.
|
||||||
if (task.type === 'serve' && task.payload?.repo_id) {
|
if (task.type === 'serve' && task.payload?.repo_id) {
|
||||||
items.push({ label: 'Edit in serve panel', action: 'edit-panel', tooltip: 'Open the full Serve config panel pre-filled with this task — pick a different backend, change GPUs, edit env vars, then Launch from there', custom: () => _openEdit() });
|
items.push({ group: 'edit', label: 'Edit & relaunch', action: 'edit-panel', tooltip: 'Open the Serve config panel pre-filled with this task — pick a different backend, change GPUs, edit env vars or the raw cmd, then Launch.', custom: () => _openEdit() });
|
||||||
}
|
}
|
||||||
// Save serve — save current launch config as a preset.
|
|
||||||
if (task.type === 'serve' && task.payload?._cmd) {
|
if (task.type === 'serve' && task.payload?._cmd) {
|
||||||
items.push({ label: 'Save serve', action: 'save', custom: () => {
|
items.push({ group: 'edit', label: 'Save serve', action: 'save', custom: () => {
|
||||||
if (!_saveTaskAsPreset(task)) { uiModule.showToast('Already saved'); return; }
|
if (!_saveTaskAsPreset(task)) { uiModule.showToast('Already saved'); return; }
|
||||||
uiModule.showToast('Saved to presets');
|
uiModule.showToast('Saved to presets');
|
||||||
_renderRunningTab();
|
_renderRunningTab();
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
// Edit command — only meaningful for serve tasks that aren't running.
|
// ── Endpoint section ────────────────────────────────────────
|
||||||
// Lets the user tweak flags after a crash/error and relaunch.
|
|
||||||
if (task.type === 'serve' && task.status !== 'running' && task.payload?._cmd) {
|
|
||||||
items.push({ label: 'Edit cmd & relaunch', action: 'edit', tooltip: 'Edit the raw vllm/llama-server cmd string in a dialog and relaunch immediately on the same host', custom: async () => {
|
|
||||||
const newCmd = await _promptEditServeCmd(task.payload._cmd);
|
|
||||||
if (newCmd == null) return; // cancelled
|
|
||||||
try {
|
|
||||||
await fetch('/api/shell/exec', {
|
|
||||||
method: 'POST', credentials: 'same-origin',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ command: _tmuxGracefulKill(task) }),
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
_removeTask(task.sessionId);
|
|
||||||
// Relaunch on the task's OWN host, not the current global selection.
|
|
||||||
_launchServeTask(task.name, task.payload.repo_id, newCmd, task.payload._fields, task.remoteHost || '');
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
// Manual endpoint registration — fallback for when auto-add fails
|
// Manual endpoint registration — fallback for when auto-add fails
|
||||||
// (e.g. probe timeout on a remote that's slow). Forces adding this
|
// (e.g. probe timeout on a remote that's slow). Forces adding this
|
||||||
// serve to the model-endpoints list regardless of prior flag state.
|
// serve to the model-endpoints list regardless of prior flag state.
|
||||||
if (task.type === 'serve' && task.payload?._cmd) {
|
if (task.type === 'serve' && task.payload?._cmd) {
|
||||||
items.push({ label: 'Register endpoint', action: 'register-endpoint', custom: async () => {
|
items.push({ group: 'endpoint', label: 'Register endpoint', action: 'register-endpoint', custom: async () => {
|
||||||
const host = _connectHostFromRemote(task.remoteHost);
|
const host = _connectHostFromRemote(task.remoteHost);
|
||||||
const portMatch = task.payload?._cmd?.match(/--port\s+(\d+)/);
|
const portMatch = task.payload?._cmd?.match(/--port\s+(\d+)/);
|
||||||
const port = portMatch ? portMatch[1] : '8000';
|
const port = portMatch ? portMatch[1] : '8000';
|
||||||
@@ -2194,31 +2180,32 @@ export function _renderRunningTab() {
|
|||||||
}
|
}
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
// ── Copy section ────────────────────────────────────────────
|
||||||
if (_isWindows(task)) {
|
if (_isWindows(task)) {
|
||||||
const host = task.remoteHost;
|
const host = task.remoteHost;
|
||||||
const sd = host ? '$env:TEMP\\odysseus-sessions' : '$env:TEMP\\odysseus-tmux';
|
const sd = host ? '$env:TEMP\\odysseus-sessions' : '$env:TEMP\\odysseus-tmux';
|
||||||
const logCmd = host
|
const logCmd = host
|
||||||
? `ssh ${_sshPrefix(_getPort(task))}${host} "powershell -Command \\"Get-Content '${sd}\\${task.sessionId}.log' -Wait\\""`
|
? `ssh ${_sshPrefix(_getPort(task))}${host} "powershell -Command \\"Get-Content '${sd}\\${task.sessionId}.log' -Wait\\""`
|
||||||
: `powershell -Command "Get-Content (Join-Path $env:TEMP 'odysseus-tmux\\${task.sessionId}.log') -Wait"`;
|
: `powershell -Command "Get-Content (Join-Path $env:TEMP 'odysseus-tmux\\${task.sessionId}.log') -Wait"`;
|
||||||
items.push({ label: 'Copy log cmd', action: 'copy-tmux', custom: () => {
|
items.push({ group: 'copy', label: 'Copy log cmd', action: 'copy-tmux', custom: () => {
|
||||||
_copyText(logCmd);
|
_copyText(logCmd);
|
||||||
}});
|
}});
|
||||||
} else {
|
} else {
|
||||||
// Just the tmux command itself — no ssh wrapper.
|
// Just the tmux command itself — no ssh wrapper.
|
||||||
const tmuxAttach = `tmux attach -t ${task.sessionId}`;
|
const tmuxAttach = `tmux attach -t ${task.sessionId}`;
|
||||||
items.push({ label: 'Copy tmux', action: 'copy-tmux', custom: () => {
|
items.push({ group: 'copy', label: 'Copy tmux', action: 'copy-tmux', custom: () => {
|
||||||
_copyText(tmuxAttach);
|
_copyText(tmuxAttach);
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
if (_shouldOfferCrashReport(task)) {
|
if (_shouldOfferCrashReport(task)) {
|
||||||
items.push({ label: 'Copy crash report', action: 'copy-crash-report', custom: () => {
|
items.push({ group: 'copy', label: 'Copy crash report', action: 'copy-crash-report', custom: () => {
|
||||||
const out = (el.querySelector('.cookbook-output-pre')?.textContent || task.output || '');
|
const out = (el.querySelector('.cookbook-output-pre')?.textContent || task.output || '');
|
||||||
_copyText(_buildCrashReport(task, out));
|
_copyText(_buildCrashReport(task, out));
|
||||||
uiModule.showToast('Copied crash report');
|
uiModule.showToast('Copied crash report');
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
// Copy the last 50 lines of the task's output/log.
|
// Copy the last 50 lines of the task's output/log.
|
||||||
items.push({ label: 'Copy last 50 lines', action: 'copy-log', custom: () => {
|
items.push({ group: 'copy', label: 'Copy last 50 lines', action: 'copy-log', custom: () => {
|
||||||
const out = (el.querySelector('.cookbook-output-pre')?.textContent || task.output || '');
|
const out = (el.querySelector('.cookbook-output-pre')?.textContent || task.output || '');
|
||||||
const last = out.split('\n').slice(-50).join('\n');
|
const last = out.split('\n').slice(-50).join('\n');
|
||||||
if (!last.trim()) {
|
if (!last.trim()) {
|
||||||
@@ -2232,8 +2219,10 @@ export function _renderRunningTab() {
|
|||||||
// the live tmux session and (for serve tasks) deletes the
|
// the live tmux session and (for serve tasks) deletes the
|
||||||
// matching model-endpoint, THEN animates the task card out.
|
// matching model-endpoint, THEN animates the task card out.
|
||||||
// Just "Remove" hid that it stops the live serve too.
|
// Just "Remove" hid that it stops the live serve too.
|
||||||
|
// ── Danger section ──────────────────────────────────────────
|
||||||
const _isLive = task.type === 'serve' && ['running', 'ready', 'loading', 'warming', 'starting'].includes(task.status || '');
|
const _isLive = task.type === 'serve' && ['running', 'ready', 'loading', 'warming', 'starting'].includes(task.status || '');
|
||||||
items.push({
|
items.push({
|
||||||
|
group: 'danger',
|
||||||
label: _isLive ? 'Stop and remove' : 'Remove',
|
label: _isLive ? 'Stop and remove' : 'Remove',
|
||||||
action: 'kill',
|
action: 'kill',
|
||||||
tooltip: _isLive
|
tooltip: _isLive
|
||||||
@@ -2241,10 +2230,8 @@ export function _renderRunningTab() {
|
|||||||
: 'Remove this row',
|
: 'Remove this row',
|
||||||
danger: true,
|
danger: true,
|
||||||
});
|
});
|
||||||
// Cancel = mobile-only dismiss item. Same pattern as the email kebab:
|
// Cancel = mobile-only dismiss item. Same pattern as the email kebab.
|
||||||
// the `dropdown-cancel-mobile` class is hidden on desktop and styled
|
items.push({ group: 'danger', label: 'Cancel', action: 'cancel', mobileOnly: true, custom: () => {} });
|
||||||
// as a separated bottom row on mobile (border-top + extra padding).
|
|
||||||
items.push({ label: 'Cancel', action: 'cancel', mobileOnly: true, custom: () => {} });
|
|
||||||
|
|
||||||
const _MENU_ICONS = {
|
const _MENU_ICONS = {
|
||||||
'start-now': '<polygon points="6 4 20 12 6 20 6 4"/>',
|
'start-now': '<polygon points="6 4 20 12 6 20 6 4"/>',
|
||||||
@@ -2261,7 +2248,18 @@ export function _renderRunningTab() {
|
|||||||
kill: '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
|
kill: '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
|
||||||
cancel: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
cancel: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
||||||
};
|
};
|
||||||
|
let _lastGroup = null;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
// Insert a thin divider whenever the group changes, so the
|
||||||
|
// user can visually scan Run / Edit / Endpoint / Copy / Danger
|
||||||
|
// blocks instead of one long undifferentiated list.
|
||||||
|
if (item.group && _lastGroup && item.group !== _lastGroup) {
|
||||||
|
const sep = document.createElement('div');
|
||||||
|
sep.className = 'cookbook-dropdown-divider';
|
||||||
|
sep.style.cssText = 'height:1px;margin:4px 6px;background:color-mix(in srgb, var(--fg) 12%, transparent);pointer-events:none;';
|
||||||
|
dropdown.appendChild(sep);
|
||||||
|
}
|
||||||
|
_lastGroup = item.group || _lastGroup;
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'dropdown-item-compact'
|
div.className = 'dropdown-item-compact'
|
||||||
+ (item.danger ? ' cookbook-dropdown-danger' : '')
|
+ (item.danger ? ' cookbook-dropdown-danger' : '')
|
||||||
|
|||||||
Reference in New Issue
Block a user