feat: add task queue sidebar and resume functionality for CLI sessions

- Implemented task queue sidebar for managing active tasks with filtering options.
- Added functionality to close notification sidebar when opening task queue.
- Enhanced CLI history view to support resuming previous sessions with optional prompts.
- Updated CLI executor to handle resuming sessions for Codex, Gemini, and Qwen tools.
- Introduced utility functions for finding CLI history directories recursively.
- Improved task queue data management and rendering logic.
This commit is contained in:
catlog22
2025-12-13 11:51:53 +08:00
parent 335f5e9ec6
commit 93d3df1e08
14 changed files with 2000 additions and 128 deletions

View File

@@ -934,3 +934,856 @@
font-weight: 500;
}
/* ==========================================
NOTIFICATION SIDEBAR (Right-Side Toolbar)
========================================== */
/* Sidebar Container */
.notif-sidebar {
position: fixed;
top: 0;
right: 0;
width: 380px;
max-width: 90vw;
height: 100vh;
background: hsl(var(--card));
border-left: 1px solid hsl(var(--border));
box-shadow: -4px 0 24px rgb(0 0 0 / 0.15);
z-index: 1100;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.notif-sidebar.open {
transform: translateX(0);
}
/* Sidebar Header */
.notif-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--card));
}
.notif-sidebar-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.notif-title-icon {
font-size: 1.25rem;
}
.notif-count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 6px;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-radius: 11px;
font-size: 0.75rem;
font-weight: 600;
}
.notif-sidebar-close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
}
.notif-sidebar-close:hover {
background: hsl(var(--hover));
color: hsl(var(--foreground));
}
/* Sidebar Actions */
.notif-sidebar-actions {
display: flex;
gap: 8px;
padding: 12px 20px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.3);
}
.notif-action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 6px;
font-size: 0.8125rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
}
.notif-action-btn:hover {
background: hsl(var(--hover));
color: hsl(var(--foreground));
border-color: hsl(var(--primary) / 0.5);
}
/* Sidebar Content */
.notif-sidebar-content {
flex: 1;
overflow-y: auto;
padding: 12px;
}
/* Empty State */
.notif-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
text-align: center;
}
.notif-empty-icon {
font-size: 3rem;
opacity: 0.3;
margin-bottom: 16px;
}
.notif-empty-text {
font-size: 1rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
margin-bottom: 8px;
}
.notif-empty-hint {
font-size: 0.8125rem;
color: hsl(var(--muted-foreground) / 0.7);
}
/* Notification Items */
.notif-item {
padding: 12px 14px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.15s;
}
.notif-item:hover {
border-color: hsl(var(--primary) / 0.3);
box-shadow: 0 2px 8px rgb(0 0 0 / 0.06);
}
.notif-item.read {
opacity: 0.7;
}
.notif-item.type-info {
border-left: 3px solid hsl(var(--primary));
}
.notif-item.type-success {
border-left: 3px solid hsl(var(--success));
}
.notif-item.type-warning {
border-left: 3px solid hsl(var(--warning));
}
.notif-item.type-error {
border-left: 3px solid hsl(var(--destructive));
}
.notif-item-header {
display: flex;
align-items: flex-start;
gap: 10px;
}
.notif-icon {
font-size: 1.125rem;
flex-shrink: 0;
}
.notif-item-content {
flex: 1;
min-width: 0;
}
.notif-message {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
line-height: 1.4;
word-break: break-word;
}
.notif-source {
display: inline-block;
margin-top: 4px;
padding: 2px 8px;
background: hsl(var(--muted));
border-radius: 4px;
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
font-family: var(--font-mono);
}
/* Notification Details */
.notif-details {
margin-top: 10px;
padding: 10px 12px;
background: hsl(var(--muted) / 0.5);
border-radius: 6px;
font-size: 0.8125rem;
color: hsl(var(--muted-foreground));
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-mono);
}
/* JSON Formatted Details */
.notif-details-json {
margin-top: 10px;
padding: 10px 12px;
background: hsl(var(--muted) / 0.5);
border-radius: 6px;
font-size: 0.8125rem;
line-height: 1.6;
font-family: var(--font-mono);
}
.json-field {
padding: 2px 0;
}
.json-key {
color: hsl(var(--primary));
font-weight: 500;
}
.json-value {
color: hsl(var(--foreground));
}
.json-string {
color: hsl(142 71% 45%);
}
.json-number {
color: hsl(217 91% 60%);
}
.json-bool {
color: hsl(280 65% 60%);
}
.json-null {
color: hsl(var(--muted-foreground));
font-style: italic;
}
.json-object {
color: hsl(var(--muted-foreground));
}
.json-empty {
color: hsl(var(--muted-foreground));
font-style: italic;
}
.json-more {
color: hsl(var(--muted-foreground));
font-style: italic;
padding-top: 4px;
}
.json-array-item {
padding: 2px 0;
}
.json-index {
color: hsl(var(--muted-foreground));
font-size: 0.75rem;
margin-right: 6px;
}
/* Notification Meta */
.notif-meta {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid hsl(var(--border) / 0.5);
}
.notif-time {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
/* Toggle Button (Right Edge) */
.notif-sidebar-toggle {
position: fixed;
top: 50%;
right: 0;
transform: translateY(-50%);
width: 40px;
height: 64px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-right: none;
border-radius: 8px 0 0 8px;
box-shadow: -2px 0 8px rgb(0 0 0 / 0.1);
cursor: pointer;
z-index: 1050;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
transition: all 0.2s;
}
.notif-sidebar-toggle:hover {
width: 48px;
background: hsl(var(--hover));
}
.notif-sidebar-toggle.hidden {
transform: translateY(-50%) translateX(100%);
opacity: 0;
pointer-events: none;
}
.toggle-icon {
font-size: 1.25rem;
}
.toggle-badge {
display: none;
min-width: 18px;
height: 18px;
padding: 0 5px;
background: hsl(var(--destructive));
color: white;
border-radius: 9px;
font-size: 0.6875rem;
font-weight: 600;
align-items: center;
justify-content: center;
}
.toggle-badge[style*="display: flex"] {
display: flex;
}
/* Overlay */
.notif-sidebar-overlay {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 0.4);
z-index: 1090;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.notif-sidebar-overlay.show {
opacity: 1;
visibility: visible;
}
/* Toast Notification */
.notif-toast {
position: fixed;
bottom: 24px;
right: 24px;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 18px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 8px;
box-shadow: 0 8px 24px rgb(0 0 0 / 0.2);
z-index: 1200;
transform: translateY(100px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.notif-toast.show {
transform: translateY(0);
opacity: 1;
}
.notif-toast.type-info {
border-left: 3px solid hsl(var(--primary));
}
.notif-toast.type-success {
border-left: 3px solid hsl(var(--success));
}
.notif-toast.type-warning {
border-left: 3px solid hsl(var(--warning));
}
.notif-toast.type-error {
border-left: 3px solid hsl(var(--destructive));
}
.toast-icon {
font-size: 1.25rem;
}
.toast-message {
font-size: 0.875rem;
color: hsl(var(--foreground));
max-width: 280px;
}
/* Responsive */
@media (max-width: 480px) {
.notif-sidebar {
width: 100vw;
max-width: 100vw;
}
.notif-sidebar-toggle {
top: auto;
bottom: 80px;
transform: translateY(0);
}
.notif-sidebar-toggle.hidden {
transform: translateX(100%);
}
.notif-toast {
left: 16px;
right: 16px;
bottom: 16px;
}
.toast-message {
max-width: none;
}
}
/* ==========================================
TASK QUEUE SIDEBAR (Right-Side Toolbar)
========================================== */
/* Sidebar Container */
.task-queue-sidebar {
position: fixed;
top: 0;
right: 0;
width: 400px;
max-width: 90vw;
height: 100vh;
background: hsl(var(--card));
border-left: 1px solid hsl(var(--border));
box-shadow: -4px 0 24px rgb(0 0 0 / 0.15);
z-index: 1100;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.task-queue-sidebar.open {
transform: translateX(0);
}
/* Sidebar Header */
.task-queue-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--card));
}
.task-queue-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.task-queue-title-icon {
font-size: 1.25rem;
}
.task-queue-count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 6px;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-radius: 11px;
font-size: 0.75rem;
font-weight: 600;
}
.task-queue-close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
}
.task-queue-close:hover {
background: hsl(var(--hover));
color: hsl(var(--foreground));
}
/* Filters */
.task-queue-filters {
display: flex;
gap: 6px;
padding: 12px 20px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.3);
}
.task-filter-btn {
padding: 6px 12px;
background: transparent;
border: 1px solid hsl(var(--border));
border-radius: 6px;
font-size: 0.8125rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
}
.task-filter-btn:hover {
background: hsl(var(--hover));
color: hsl(var(--foreground));
}
.task-filter-btn.active {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-color: hsl(var(--primary));
}
/* Sidebar Content */
.task-queue-content {
flex: 1;
overflow-y: auto;
padding: 12px;
}
/* Empty State */
.task-queue-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
text-align: center;
}
.task-queue-empty-icon {
font-size: 3rem;
opacity: 0.3;
margin-bottom: 16px;
}
.task-queue-empty-text {
font-size: 1rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
margin-bottom: 8px;
}
.task-queue-empty-hint {
font-size: 0.8125rem;
color: hsl(var(--muted-foreground) / 0.7);
}
/* Task Queue Items */
.task-queue-item {
padding: 14px 16px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 10px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.15s;
}
.task-queue-item:hover {
border-color: hsl(var(--primary) / 0.4);
box-shadow: 0 2px 8px rgb(0 0 0 / 0.08);
transform: translateX(-2px);
}
.task-queue-item.status-in_progress {
border-left: 4px solid hsl(var(--warning));
background: hsl(var(--warning) / 0.05);
}
.task-queue-item.status-pending {
border-left: 4px solid hsl(var(--muted-foreground));
}
.task-queue-item.status-completed {
border-left: 4px solid hsl(var(--success));
opacity: 0.7;
}
.task-queue-item.status-skipped {
border-left: 4px solid hsl(var(--muted-foreground) / 0.5);
opacity: 0.5;
}
.task-queue-item-header {
display: flex;
align-items: flex-start;
gap: 12px;
}
.task-queue-status-icon {
font-size: 1.25rem;
flex-shrink: 0;
margin-top: 2px;
}
.task-queue-item-info {
flex: 1;
min-width: 0;
}
.task-queue-item-title {
display: block;
font-size: 0.9rem;
font-weight: 500;
color: hsl(var(--foreground));
line-height: 1.4;
word-break: break-word;
}
.task-queue-item-id {
display: block;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
font-family: var(--font-mono);
margin-top: 4px;
}
.task-queue-item-meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.task-queue-session-tag {
padding: 3px 8px;
background: hsl(var(--muted));
border-radius: 4px;
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
font-family: var(--font-mono);
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-queue-type-badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 0.6875rem;
font-weight: 500;
text-transform: uppercase;
}
.task-queue-type-badge.type-workflow {
background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
}
.task-queue-type-badge.type-lite {
background: hsl(var(--success) / 0.15);
color: hsl(var(--success));
}
.task-queue-type-badge.type-tdd {
background: hsl(280 65% 60% / 0.15);
color: hsl(280 65% 60%);
}
.task-queue-type-badge.type-test {
background: hsl(var(--warning) / 0.15);
color: hsl(var(--warning));
}
.task-queue-item-scope {
margin-top: 8px;
padding: 6px 10px;
background: hsl(var(--muted) / 0.5);
border-radius: 6px;
}
.task-queue-item-scope code {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
font-family: var(--font-mono);
word-break: break-all;
}
/* Toggle Button (Below Notification Toggle) */
.task-queue-toggle {
position: fixed;
top: calc(50% + 40px);
right: 0;
transform: translateY(-50%);
width: 40px;
height: 64px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-right: none;
border-radius: 8px 0 0 8px;
box-shadow: -2px 0 8px rgb(0 0 0 / 0.1);
cursor: pointer;
z-index: 1050;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
transition: all 0.2s;
}
.task-queue-toggle:hover {
width: 48px;
background: hsl(var(--hover));
}
.task-queue-toggle.hidden {
transform: translateY(-50%) translateX(100%);
opacity: 0;
pointer-events: none;
}
.task-queue-toggle .toggle-icon {
font-size: 1.25rem;
}
.task-queue-toggle .toggle-badge {
display: none;
min-width: 18px;
height: 18px;
padding: 0 5px;
background: hsl(var(--warning));
color: white;
border-radius: 9px;
font-size: 0.6875rem;
font-weight: 600;
align-items: center;
justify-content: center;
}
.task-queue-toggle .toggle-badge[style*="display: flex"] {
display: flex;
}
.task-queue-toggle .toggle-badge.has-active {
background: hsl(var(--warning));
animation: taskPulse 2s ease-in-out infinite;
}
@keyframes taskPulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* Overlay */
.task-queue-overlay {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 0.4);
z-index: 1090;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.task-queue-overlay.show {
opacity: 1;
visibility: visible;
}
/* Adjust notification toggle position to be above task toggle */
.notif-sidebar-toggle {
top: calc(50% - 40px);
}
/* Responsive for Task Queue */
@media (max-width: 480px) {
.task-queue-sidebar {
width: 100vw;
max-width: 100vw;
}
.task-queue-toggle {
top: auto;
bottom: 80px;
transform: translateY(0);
}
.task-queue-toggle.hidden {
transform: translateX(100%);
}
.notif-sidebar-toggle {
bottom: 150px;
}
}

View File

@@ -469,6 +469,22 @@
border-radius: 0.25rem;
}
.history-source-dir {
font-size: 0.625rem;
font-weight: 500;
padding: 0.1875rem 0.5rem;
background: hsl(var(--accent));
color: hsl(var(--accent-foreground));
border-radius: 0.25rem;
display: inline-flex;
align-items: center;
gap: 0.25rem;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-status {
display: flex;
align-items: center;
@@ -2028,3 +2044,85 @@
color: hsl(var(--foreground));
border-color: hsl(var(--primary) / 0.3);
}
/* ========================================
* Resume Session Styles
* ======================================== */
/* Resume Badge */
.history-resume-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.1875rem 0.375rem;
background: hsl(var(--primary) / 0.12);
color: hsl(var(--primary));
border-radius: 0.25rem;
font-size: 0.625rem;
}
/* Resume Item Highlight */
.history-item-resume {
border-left: 3px solid hsl(var(--primary) / 0.5);
}
.history-item-resume:hover {
border-left-color: hsl(var(--primary));
}
/* History ID Display */
.history-id {
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
font-size: 0.625rem;
color: hsl(var(--muted-foreground) / 0.7);
}
/* Resume Button */
.btn-resume {
color: hsl(var(--primary));
}
.btn-resume:hover {
background: hsl(var(--primary) / 0.1);
color: hsl(var(--primary));
}
/* Resume Modal */
.resume-modal p {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
margin-bottom: 1rem;
}
.resume-prompt-input {
width: 100%;
padding: 0.75rem;
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
background: hsl(var(--background));
color: hsl(var(--foreground));
font-size: 0.875rem;
font-family: inherit;
resize: vertical;
min-height: 80px;
transition: all 0.2s ease;
}
.resume-prompt-input:focus {
outline: none;
border-color: hsl(var(--primary));
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.15);
}
.resume-prompt-input::placeholder {
color: hsl(var(--muted-foreground) / 0.7);
}
.resume-modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid hsl(var(--border));
}

View File

@@ -28,9 +28,13 @@ async function loadCliHistory(options = {}) {
}
}
async function loadExecutionDetail(executionId) {
async function loadExecutionDetail(executionId, sourceDir) {
try {
const url = `/api/cli/execution?path=${encodeURIComponent(projectPath)}&id=${encodeURIComponent(executionId)}`;
// If sourceDir provided, use it to build the correct path
const basePath = sourceDir && sourceDir !== '.'
? projectPath + '/' + sourceDir
: projectPath;
const url = `/api/cli/execution?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`;
const response = await fetch(url);
if (!response.ok) throw new Error('Execution not found');
return await response.json();
@@ -158,8 +162,8 @@ function renderToolFilter() {
}
// ========== Execution Detail Modal ==========
async function showExecutionDetail(executionId) {
const detail = await loadExecutionDetail(executionId);
async function showExecutionDetail(executionId, sourceDir) {
const detail = await loadExecutionDetail(executionId, sourceDir);
if (!detail) {
showRefreshToast('Execution not found', 'error');
return;

View File

@@ -1,80 +1,132 @@
// ==========================================
// GLOBAL NOTIFICATION SYSTEM
// GLOBAL NOTIFICATION SYSTEM - Right Sidebar
// ==========================================
// Floating notification panel accessible from any view
// Right-side slide-out toolbar for notifications and quick actions
/**
* Initialize global notification panel
* Initialize global notification sidebar
*/
function initGlobalNotifications() {
// Create FAB and panel if not exists
if (!document.getElementById('globalNotificationFab')) {
const fabHtml = `
<div class="global-notif-fab" id="globalNotificationFab" onclick="toggleGlobalNotifications()" title="Notifications">
<span class="fab-icon">🔔</span>
<span class="fab-badge" id="globalNotifBadge">0</span>
</div>
<div class="global-notif-panel" id="globalNotificationPanel">
<div class="global-notif-header">
<span class="global-notif-title">🔔 Notifications</span>
<button class="global-notif-close" onclick="toggleGlobalNotifications()">×</button>
</div>
<div class="global-notif-actions">
<button class="notif-action-btn" onclick="clearGlobalNotifications()">
<span>🗑️</span> Clear All
// Create sidebar if not exists
if (!document.getElementById('notifSidebar')) {
const sidebarHtml = `
<div class="notif-sidebar" id="notifSidebar">
<div class="notif-sidebar-header">
<div class="notif-sidebar-title">
<span class="notif-title-icon">🔔</span>
<span>Notifications</span>
<span class="notif-count-badge" id="notifCountBadge">0</span>
</div>
<button class="notif-sidebar-close" onclick="toggleNotifSidebar()" title="Close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</div>
<div class="global-notif-list" id="globalNotificationList">
<div class="global-notif-empty">
<span>No notifications</span>
<p>System events and task updates will appear here</p>
<div class="notif-sidebar-actions">
<button class="notif-action-btn" onclick="markAllNotificationsRead()" title="Mark all read">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 6L9 17l-5-5"/>
</svg>
<span>Mark Read</span>
</button>
<button class="notif-action-btn" onclick="clearGlobalNotifications()" title="Clear all">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
</svg>
<span>Clear All</span>
</button>
</div>
<div class="notif-sidebar-content" id="notifSidebarContent">
<div class="notif-empty-state">
<div class="notif-empty-icon">🔔</div>
<div class="notif-empty-text">No notifications</div>
<div class="notif-empty-hint">System events and task updates will appear here</div>
</div>
</div>
</div>
<div class="notif-sidebar-toggle" id="notifSidebarToggle" onclick="toggleNotifSidebar()" title="Notifications">
<span class="toggle-icon">🔔</span>
<span class="toggle-badge" id="notifToggleBadge"></span>
</div>
<div class="notif-sidebar-overlay" id="notifSidebarOverlay" onclick="toggleNotifSidebar()"></div>
`;
const container = document.createElement('div');
container.id = 'globalNotificationContainer';
container.innerHTML = fabHtml;
container.id = 'notifSidebarContainer';
container.innerHTML = sidebarHtml;
document.body.appendChild(container);
}
renderGlobalNotifications();
updateGlobalNotifBadge();
}
/**
* Toggle notification panel visibility
* Toggle notification sidebar visibility
*/
function toggleGlobalNotifications() {
function toggleNotifSidebar() {
isNotificationPanelVisible = !isNotificationPanelVisible;
const panel = document.getElementById('globalNotificationPanel');
const fab = document.getElementById('globalNotificationFab');
if (panel && fab) {
const sidebar = document.getElementById('notifSidebar');
const overlay = document.getElementById('notifSidebarOverlay');
const toggle = document.getElementById('notifSidebarToggle');
if (sidebar && overlay && toggle) {
if (isNotificationPanelVisible) {
panel.classList.add('show');
fab.classList.add('active');
sidebar.classList.add('open');
overlay.classList.add('show');
toggle.classList.add('hidden');
// Mark notifications as read when opened
markAllNotificationsRead();
} else {
panel.classList.remove('show');
fab.classList.remove('active');
sidebar.classList.remove('open');
overlay.classList.remove('show');
toggle.classList.remove('hidden');
}
}
}
// Backward compatibility alias
function toggleGlobalNotifications() {
toggleNotifSidebar();
}
/**
* Add a global notification
* @param {string} type - 'info', 'success', 'warning', 'error'
* @param {string} message - Main notification message
* @param {string} details - Optional details
* @param {string} source - Optional source identifier (e.g., 'explorer', 'mcp')
* @param {string|object} details - Optional details (string or object)
* @param {string} source - Optional source identifier
*/
function addGlobalNotification(type, message, details = null, source = null) {
// Format details if it's an object
let formattedDetails = details;
if (details && typeof details === 'object') {
formattedDetails = formatNotificationJson(details);
} else if (typeof details === 'string') {
// Try to parse and format if it looks like JSON
const trimmed = details.trim();
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try {
const parsed = JSON.parse(trimmed);
formattedDetails = formatNotificationJson(parsed);
} catch (e) {
// Not valid JSON, use as-is
formattedDetails = details;
}
}
}
const notification = {
id: Date.now(),
type,
message,
details,
details: formattedDetails,
source,
timestamp: new Date().toISOString(),
read: false
@@ -101,6 +153,69 @@ function addGlobalNotification(type, message, details = null, source = null) {
}
}
/**
* Format JSON object for notification display
* @param {Object} obj - Object to format
* @returns {string} HTML formatted string
*/
function formatNotificationJson(obj) {
if (obj === null || obj === undefined) return '';
if (typeof obj !== 'object') return String(obj);
// Handle arrays
if (Array.isArray(obj)) {
if (obj.length === 0) return '<span class="json-empty">(empty array)</span>';
const items = obj.slice(0, 5).map((item, i) => {
const itemStr = typeof item === 'object' ? JSON.stringify(item) : String(item);
const truncated = itemStr.length > 60 ? itemStr.substring(0, 57) + '...' : itemStr;
return `<div class="json-array-item"><span class="json-index">[${i}]</span> ${escapeHtml(truncated)}</div>`;
});
if (obj.length > 5) {
items.push(`<div class="json-more">... +${obj.length - 5} more items</div>`);
}
return items.join('');
}
// Handle objects
const entries = Object.entries(obj);
if (entries.length === 0) return '<span class="json-empty">(empty object)</span>';
const lines = entries.slice(0, 8).map(([key, val]) => {
let valStr;
let valClass = 'json-value';
if (val === null) {
valStr = 'null';
valClass = 'json-null';
} else if (val === undefined) {
valStr = 'undefined';
valClass = 'json-null';
} else if (typeof val === 'boolean') {
valStr = val ? 'true' : 'false';
valClass = 'json-bool';
} else if (typeof val === 'number') {
valStr = String(val);
valClass = 'json-number';
} else if (typeof val === 'object') {
valStr = JSON.stringify(val);
if (valStr.length > 50) valStr = valStr.substring(0, 47) + '...';
valClass = 'json-object';
} else {
valStr = String(val);
if (valStr.length > 60) valStr = valStr.substring(0, 57) + '...';
valClass = 'json-string';
}
return `<div class="json-field"><span class="json-key">${escapeHtml(key)}:</span> <span class="${valClass}">${escapeHtml(valStr)}</span></div>`;
});
if (entries.length > 8) {
lines.push(`<div class="json-more">... +${entries.length - 8} more fields</div>`);
}
return lines.join('');
}
/**
* Show a brief toast notification
*/
@@ -111,11 +226,11 @@ function showNotificationToast(notification) {
'warning': '⚠️',
'error': '❌'
}[notification.type] || '';
// Remove existing toast
const existing = document.querySelector('.notif-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = `notif-toast type-${notification.type}`;
toast.innerHTML = `
@@ -123,10 +238,10 @@ function showNotificationToast(notification) {
<span class="toast-message">${escapeHtml(notification.message)}</span>
`;
document.body.appendChild(toast);
// Animate in
requestAnimationFrame(() => toast.classList.add('show'));
// Auto-remove
setTimeout(() => {
toast.classList.remove('show');
@@ -135,41 +250,55 @@ function showNotificationToast(notification) {
}
/**
* Render notification list
* Render notification list in sidebar
*/
function renderGlobalNotifications() {
const listEl = document.getElementById('globalNotificationList');
if (!listEl) return;
const contentEl = document.getElementById('notifSidebarContent');
if (!contentEl) return;
if (globalNotificationQueue.length === 0) {
listEl.innerHTML = `
<div class="global-notif-empty">
<span>No notifications</span>
<p>System events and task updates will appear here</p>
contentEl.innerHTML = `
<div class="notif-empty-state">
<div class="notif-empty-icon">🔔</div>
<div class="notif-empty-text">No notifications</div>
<div class="notif-empty-hint">System events and task updates will appear here</div>
</div>
`;
return;
}
listEl.innerHTML = globalNotificationQueue.map(notif => {
contentEl.innerHTML = globalNotificationQueue.map(notif => {
const typeIcon = {
'info': '',
'success': '✅',
'warning': '⚠️',
'error': '❌'
}[notif.type] || '';
const time = formatNotifTime(notif.timestamp);
const sourceLabel = notif.source ? `<span class="notif-source">${notif.source}</span>` : '';
const sourceLabel = notif.source ? `<span class="notif-source">${escapeHtml(notif.source)}</span>` : '';
// Details may already be HTML formatted or plain text
let detailsHtml = '';
if (notif.details) {
// Check if details is already HTML formatted (contains our json-* classes)
if (typeof notif.details === 'string' && notif.details.includes('class="json-')) {
detailsHtml = `<div class="notif-details-json">${notif.details}</div>`;
} else {
detailsHtml = `<div class="notif-details">${escapeHtml(String(notif.details))}</div>`;
}
}
return `
<div class="global-notif-item type-${notif.type} ${notif.read ? 'read' : ''}" data-id="${notif.id}">
<div class="notif-item type-${notif.type} ${notif.read ? 'read' : ''}" data-id="${notif.id}">
<div class="notif-item-header">
<span class="notif-icon">${typeIcon}</span>
<span class="notif-message">${escapeHtml(notif.message)}</span>
${sourceLabel}
<div class="notif-item-content">
<span class="notif-message">${escapeHtml(notif.message)}</span>
${sourceLabel}
</div>
</div>
${notif.details ? `<div class="notif-details">${escapeHtml(notif.details)}</div>` : ''}
${detailsHtml}
<div class="notif-meta">
<span class="notif-time">${time}</span>
</div>
@@ -185,7 +314,7 @@ function formatNotifTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
@@ -193,14 +322,22 @@ function formatNotifTime(timestamp) {
}
/**
* Update notification badge
* Update notification badge counts
*/
function updateGlobalNotifBadge() {
const badge = document.getElementById('globalNotifBadge');
if (badge) {
const unreadCount = globalNotificationQueue.filter(n => !n.read).length;
badge.textContent = unreadCount;
badge.style.display = unreadCount > 0 ? 'flex' : 'none';
const unreadCount = globalNotificationQueue.filter(n => !n.read).length;
const countBadge = document.getElementById('notifCountBadge');
const toggleBadge = document.getElementById('notifToggleBadge');
if (countBadge) {
countBadge.textContent = globalNotificationQueue.length;
countBadge.style.display = globalNotificationQueue.length > 0 ? 'inline-flex' : 'none';
}
if (toggleBadge) {
toggleBadge.textContent = unreadCount;
toggleBadge.style.display = unreadCount > 0 ? 'flex' : 'none';
}
}
@@ -210,7 +347,6 @@ function updateGlobalNotifBadge() {
function clearGlobalNotifications() {
globalNotificationQueue = [];
// Clear from localStorage
if (typeof saveNotificationsToStorage === 'function') {
saveNotificationsToStorage();
}
@@ -225,7 +361,6 @@ function clearGlobalNotifications() {
function markAllNotificationsRead() {
globalNotificationQueue.forEach(n => n.read = true);
// Save to localStorage
if (typeof saveNotificationsToStorage === 'function') {
saveNotificationsToStorage();
}
@@ -233,4 +368,3 @@ function markAllNotificationsRead() {
renderGlobalNotifications();
updateGlobalNotifBadge();
}

View File

@@ -0,0 +1,265 @@
// ==========================================
// TASK QUEUE SIDEBAR - Right Sidebar
// ==========================================
// Right-side slide-out toolbar for task queue management
let isTaskQueueVisible = false;
let taskQueueData = [];
/**
* Initialize task queue sidebar
*/
function initTaskQueueSidebar() {
// Create sidebar if not exists
if (!document.getElementById('taskQueueSidebar')) {
const sidebarHtml = `
<div class="task-queue-sidebar" id="taskQueueSidebar">
<div class="task-queue-header">
<div class="task-queue-title">
<span class="task-queue-title-icon">📋</span>
<span>Task Queue</span>
<span class="task-queue-count-badge" id="taskQueueCountBadge">0</span>
</div>
<button class="task-queue-close" onclick="toggleTaskQueueSidebar()" title="Close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</div>
<div class="task-queue-filters">
<button class="task-filter-btn active" data-filter="all" onclick="filterTaskQueue('all')">All</button>
<button class="task-filter-btn" data-filter="in_progress" onclick="filterTaskQueue('in_progress')">In Progress</button>
<button class="task-filter-btn" data-filter="pending" onclick="filterTaskQueue('pending')">Pending</button>
</div>
<div class="task-queue-content" id="taskQueueContent">
<div class="task-queue-empty-state">
<div class="task-queue-empty-icon">📋</div>
<div class="task-queue-empty-text">No tasks in queue</div>
<div class="task-queue-empty-hint">Active workflow tasks will appear here</div>
</div>
</div>
</div>
<div class="task-queue-toggle" id="taskQueueToggle" onclick="toggleTaskQueueSidebar()" title="Task Queue">
<span class="toggle-icon">📋</span>
<span class="toggle-badge" id="taskQueueToggleBadge"></span>
</div>
<div class="task-queue-overlay" id="taskQueueOverlay" onclick="toggleTaskQueueSidebar()"></div>
`;
const container = document.createElement('div');
container.id = 'taskQueueContainer';
container.innerHTML = sidebarHtml;
document.body.appendChild(container);
}
updateTaskQueueData();
renderTaskQueue();
updateTaskQueueBadge();
}
/**
* Toggle task queue sidebar visibility
*/
function toggleTaskQueueSidebar() {
isTaskQueueVisible = !isTaskQueueVisible;
const sidebar = document.getElementById('taskQueueSidebar');
const overlay = document.getElementById('taskQueueOverlay');
const toggle = document.getElementById('taskQueueToggle');
if (sidebar && overlay && toggle) {
if (isTaskQueueVisible) {
// Close notification sidebar if open
if (isNotificationPanelVisible && typeof toggleNotifSidebar === 'function') {
toggleNotifSidebar();
}
sidebar.classList.add('open');
overlay.classList.add('show');
toggle.classList.add('hidden');
// Refresh data when opened
updateTaskQueueData();
renderTaskQueue();
} else {
sidebar.classList.remove('open');
overlay.classList.remove('show');
toggle.classList.remove('hidden');
}
}
}
/**
* Update task queue data from workflow data
*/
function updateTaskQueueData() {
taskQueueData = [];
// Collect tasks from active sessions
const activeSessions = workflowData.activeSessions || [];
activeSessions.forEach(session => {
const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
const sessionData = sessionDataStore[sessionKey] || session;
const tasks = sessionData.tasks || [];
tasks.forEach(task => {
taskQueueData.push({
...task,
session_id: session.session_id,
session_type: session.type || 'workflow',
session_description: session.description || session.session_id
});
});
});
// Also check lite task sessions
Object.keys(liteTaskDataStore).forEach(key => {
const liteSession = liteTaskDataStore[key];
if (liteSession && liteSession.tasks) {
liteSession.tasks.forEach(task => {
taskQueueData.push({
...task,
session_id: liteSession.session_id || key,
session_type: liteSession.type || 'lite',
session_description: liteSession.description || key
});
});
}
});
// Sort: in_progress first, then pending, then by timestamp
taskQueueData.sort((a, b) => {
const statusOrder = { 'in_progress': 0, 'pending': 1, 'completed': 2, 'skipped': 3 };
const aOrder = statusOrder[a.status] ?? 99;
const bOrder = statusOrder[b.status] ?? 99;
if (aOrder !== bOrder) return aOrder - bOrder;
return 0;
});
}
/**
* Render task queue list
*/
function renderTaskQueue(filter = 'all') {
const contentEl = document.getElementById('taskQueueContent');
if (!contentEl) return;
let filteredTasks = taskQueueData;
if (filter !== 'all') {
filteredTasks = taskQueueData.filter(t => t.status === filter);
}
if (filteredTasks.length === 0) {
contentEl.innerHTML = `
<div class="task-queue-empty-state">
<div class="task-queue-empty-icon">📋</div>
<div class="task-queue-empty-text">${filter === 'all' ? 'No tasks in queue' : `No ${filter.replace('_', ' ')} tasks`}</div>
<div class="task-queue-empty-hint">Active workflow tasks will appear here</div>
</div>
`;
return;
}
contentEl.innerHTML = filteredTasks.map(task => {
const statusIcon = {
'in_progress': '🔄',
'pending': '⏳',
'completed': '✅',
'skipped': '⏭️'
}[task.status] || '📋';
const statusClass = task.status || 'pending';
const taskId = task.task_id || task.id || 'N/A';
const title = task.title || task.description || taskId;
return `
<div class="task-queue-item status-${statusClass}" data-task-id="${escapeHtml(taskId)}" onclick="openTaskFromQueue('${escapeHtml(task.session_id)}', '${escapeHtml(taskId)}')">
<div class="task-queue-item-header">
<span class="task-queue-status-icon">${statusIcon}</span>
<div class="task-queue-item-info">
<span class="task-queue-item-title">${escapeHtml(title)}</span>
<span class="task-queue-item-id">${escapeHtml(taskId)}</span>
</div>
</div>
<div class="task-queue-item-meta">
<span class="task-queue-session-tag" title="${escapeHtml(task.session_description)}">
${escapeHtml(task.session_id)}
</span>
<span class="task-queue-type-badge type-${task.session_type}">${escapeHtml(task.session_type)}</span>
</div>
${task.scope ? `<div class="task-queue-item-scope"><code>${escapeHtml(task.scope)}</code></div>` : ''}
</div>
`;
}).join('');
}
/**
* Filter task queue
*/
function filterTaskQueue(filter) {
// Update active filter button
document.querySelectorAll('.task-filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.filter === filter);
});
renderTaskQueue(filter);
}
/**
* Open task from queue (navigate to task detail)
*/
function openTaskFromQueue(sessionId, taskId) {
// Close sidebar
toggleTaskQueueSidebar();
// Try to find and open the task
const sessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
// Check if it's a lite task session
if (liteTaskDataStore[sessionKey]) {
if (typeof openTaskDrawerForLite === 'function') {
currentSessionDetailKey = sessionKey;
openTaskDrawerForLite(sessionId, taskId);
}
} else {
// Regular workflow task
if (typeof openTaskDrawer === 'function') {
currentDrawerTasks = sessionDataStore[sessionKey]?.tasks || [];
openTaskDrawer(taskId);
}
}
}
/**
* Update task queue badge counts
*/
function updateTaskQueueBadge() {
const inProgressCount = taskQueueData.filter(t => t.status === 'in_progress').length;
const pendingCount = taskQueueData.filter(t => t.status === 'pending').length;
const activeCount = inProgressCount + pendingCount;
const countBadge = document.getElementById('taskQueueCountBadge');
const toggleBadge = document.getElementById('taskQueueToggleBadge');
if (countBadge) {
countBadge.textContent = taskQueueData.length;
countBadge.style.display = taskQueueData.length > 0 ? 'inline-flex' : 'none';
}
if (toggleBadge) {
toggleBadge.textContent = activeCount;
toggleBadge.style.display = activeCount > 0 ? 'flex' : 'none';
// Highlight if there are in-progress tasks
toggleBadge.classList.toggle('has-active', inProgressCount > 0);
}
}
/**
* Refresh task queue (called from external updates)
*/
function refreshTaskQueue() {
updateTaskQueueData();
renderTaskQueue();
updateTaskQueueBadge();
}

View File

@@ -20,6 +20,7 @@ document.addEventListener('DOMContentLoaded', async () => {
try { initHookManager(); } catch (e) { console.error('Hook Manager init failed:', e); }
try { initCliStatus(); } catch (e) { console.error('CLI Status init failed:', e); }
try { initGlobalNotifications(); } catch (e) { console.error('Global notifications init failed:', e); }
try { initTaskQueueSidebar(); } catch (e) { console.error('Task queue sidebar init failed:', e); }
try { initVersionCheck(); } catch (e) { console.error('Version check init failed:', e); }
// Initialize real-time features (WebSocket + auto-refresh)
@@ -71,6 +72,16 @@ document.addEventListener('DOMContentLoaded', async () => {
if (typeof closeHookCreateModal === 'function') {
closeHookCreateModal();
}
// Close notification sidebar if open
if (isNotificationPanelVisible && typeof toggleNotifSidebar === 'function') {
toggleNotifSidebar();
}
// Close task queue sidebar if open
if (isTaskQueueVisible && typeof toggleTaskQueueSidebar === 'function') {
toggleTaskQueueSidebar();
}
}
});
});

View File

@@ -229,6 +229,7 @@ function handleWorkflowEvent(event) {
if (typeof updateStats === 'function') updateStats();
if (typeof updateBadges === 'function') updateBadges();
if (typeof updateCarousel === 'function') updateCarousel();
if (typeof refreshTaskQueue === 'function') refreshTaskQueue();
// Re-render current view if needed
if (currentView === 'sessions' && typeof renderSessions === 'function') {

View File

@@ -1,5 +1,5 @@
// CLI History View
// Standalone view for CLI execution history
// Standalone view for CLI execution history with resume support
// ========== Rendering ==========
async function renderCliHistoryView() {
@@ -47,12 +47,21 @@ async function renderCliHistoryView() {
exec.status === 'timeout' ? 'warning' : 'error';
var duration = formatDuration(exec.duration_ms);
var timeAgo = getTimeAgo(new Date(exec.timestamp));
var isResume = exec.prompt_preview && exec.prompt_preview.includes('[Resume session');
historyHtml += '<div class="history-item" onclick="showExecutionDetail(\'' + exec.id + '\')">' +
var sourceDirHtml = exec.sourceDir && exec.sourceDir !== '.'
? '<span class="history-source-dir"><i data-lucide="folder" class="w-3 h-3"></i> ' + escapeHtml(exec.sourceDir) + '</span>'
: '';
var resumeBadge = isResume ? '<span class="history-resume-badge"><i data-lucide="rotate-ccw" class="w-3 h-3"></i></span>' : '';
historyHtml += '<div class="history-item' + (isResume ? ' history-item-resume' : '') + '" onclick="showExecutionDetail(\'' + exec.id + (exec.sourceDir ? '\',\'' + escapeHtml(exec.sourceDir) : '') + '\')">' +
'<div class="history-item-main">' +
'<div class="history-item-header">' +
'<span class="history-tool-tag tool-' + exec.tool + '">' + exec.tool + '</span>' +
'<span class="history-mode-tag">' + (exec.mode || 'analysis') + '</span>' +
resumeBadge +
sourceDirHtml +
'<span class="history-status ' + statusClass + '">' +
'<i data-lucide="' + statusIcon + '" class="w-3.5 h-3.5"></i>' +
exec.status +
@@ -62,9 +71,13 @@ async function renderCliHistoryView() {
'<div class="history-item-meta">' +
'<span class="history-time"><i data-lucide="clock" class="w-3 h-3"></i> ' + timeAgo + '</span>' +
'<span class="history-duration"><i data-lucide="timer" class="w-3 h-3"></i> ' + duration + '</span>' +
'<span class="history-id"><i data-lucide="hash" class="w-3 h-3"></i> ' + exec.id.split('-')[0] + '</span>' +
'</div>' +
'</div>' +
'<div class="history-item-actions">' +
'<button class="btn-icon btn-resume" onclick="event.stopPropagation(); promptResumeExecution(\'' + exec.id + '\', \'' + exec.tool + '\')" title="Resume">' +
'<i data-lucide="play" class="w-4 h-4"></i>' +
'</button>' +
'<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail(\'' + exec.id + '\')" title="View Details">' +
'<i data-lucide="eye" class="w-4 h-4"></i>' +
'</button>' +
@@ -130,3 +143,79 @@ async function refreshCliHistoryView() {
renderCliHistoryView();
showRefreshToast('History refreshed', 'success');
}
// ========== Resume Execution ==========
function promptResumeExecution(executionId, tool) {
var modalContent = '<div class="resume-modal">' +
'<p>Resume this ' + tool + ' session with an optional continuation prompt:</p>' +
'<textarea id="resumePromptInput" class="resume-prompt-input" placeholder="Continue from where we left off... (optional)" rows="3"></textarea>' +
'<div class="resume-modal-actions">' +
'<button class="btn btn-outline" onclick="closeModal()">Cancel</button>' +
'<button class="btn btn-primary" onclick="executeResume(\'' + executionId + '\', \'' + tool + '\')">' +
'<i data-lucide="play" class="w-4 h-4"></i> Resume' +
'</button>' +
'</div>' +
'</div>';
showModal('Resume Session', modalContent);
}
async function executeResume(executionId, tool) {
var promptInput = document.getElementById('resumePromptInput');
var additionalPrompt = promptInput ? promptInput.value.trim() : '';
closeModal();
showRefreshToast('Resuming session...', 'info');
try {
var response = await fetch('/api/cli/resume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
executionId: executionId,
tool: tool,
prompt: additionalPrompt || undefined
})
});
var result = await response.json();
if (result.success) {
showRefreshToast('Session resumed successfully', 'success');
// Refresh history to show new execution
await refreshCliHistoryView();
} else {
showRefreshToast('Resume failed: ' + (result.error || 'Unknown error'), 'error');
}
} catch (err) {
console.error('Resume failed:', err);
showRefreshToast('Resume failed: ' + err.message, 'error');
}
}
async function resumeLastSession(tool) {
showRefreshToast('Resuming last ' + (tool || '') + ' session...', 'info');
try {
var response = await fetch('/api/cli/resume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool: tool || undefined,
last: true
})
});
var result = await response.json();
if (result.success) {
showRefreshToast('Session resumed successfully', 'success');
await refreshCliHistoryView();
} else {
showRefreshToast('Resume failed: ' + (result.error || 'Unknown error'), 'error');
}
} catch (err) {
console.error('Resume failed:', err);
showRefreshToast('Resume failed: ' + err.message, 'error');
}
}