fix(multi-cli): populate multiCliPlan sessions in liteTaskDataStore

Fix task click handlers not working in multi-CLI planning detail page.

Root cause: liteTaskDataStore was not being populated with multiCliPlan
sessions during initialization, so task click handlers couldn't access
session data using currentSessionDetailKey.

Changes:
- navigation.js: Add code to populate multiCliPlan sessions in liteTaskDataStore
- notifications.js: Add code to populate multiCliPlan sessions when data refreshes

Now when task detail page loads, liteTaskDataStore contains the correct key
'multi-cli-${sessionId}' matching currentSessionDetailKey, allowing task
click handlers to find session data and open detail drawer.

Verified: Task clicks now properly open detail panel for all 7 tasks.
This commit is contained in:
catlog22
2026-01-22 15:41:01 +08:00
parent f0954b3247
commit ea04663035
22 changed files with 921 additions and 83 deletions

View File

@@ -1508,6 +1508,38 @@
transform: translateY(0);
}
.issue-pull-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
background: #1a1a1a;
color: #ffffff;
border: 1px solid #2d2d2d;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.issue-pull-btn:hover {
background: #2d2d2d;
border-color: #404040;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.issue-pull-btn:active {
transform: translateY(0);
background: #1a1a1a;
}
.issue-pull-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ==========================================
ISSUE STATS
========================================== */

View File

@@ -303,6 +303,12 @@ async function refreshWorkspace() {
liteTaskDataStore[sessionKey] = s;
});
// Populate multiCliPlan sessions
(data.liteTasks?.multiCliPlan || []).forEach(s => {
const sessionKey = `multi-cli-${s.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
liteTaskDataStore[sessionKey] = s;
});
// Update global data
window.workflowData = data;

View File

@@ -827,6 +827,12 @@ async function refreshWorkspaceData(newData) {
liteTaskDataStore[key] = s;
});
// Populate multiCliPlan sessions
(newData.liteTasks?.multiCliPlan || []).forEach(s => {
const key = `multi-cli-${s.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
liteTaskDataStore[key] = s;
});
// Update UI silently
updateStats();
updateBadges();

View File

@@ -60,6 +60,9 @@ async function checkForUpdatesNow() {
btn.disabled = true;
}
// Show checking state on badge
updateVersionBadge('checking');
// Show checking notification
console.log('[Version Check] Starting update check...');
if (typeof addGlobalNotification === 'function') {
@@ -83,6 +86,9 @@ async function checkForUpdatesNow() {
versionCheckData = data;
console.log('[Version Check] Result:', data);
// Update badge based on result
updateVersionBadge(data.hasUpdate ? 'has-update' : 'none');
if (data.hasUpdate) {
// New version available
console.log('[Version Check] Update available:', data.latestVersion);
@@ -109,6 +115,8 @@ async function checkForUpdatesNow() {
}
} catch (err) {
console.error('[Version Check] Error:', err);
// Clear badge on error
updateVersionBadge('none');
if (typeof addGlobalNotification === 'function') {
addGlobalNotification(
'error',
@@ -154,6 +162,9 @@ async function checkForUpdates() {
versionCheckData = await res.json();
// Update badge
updateVersionBadge(versionCheckData.hasUpdate ? 'has-update' : 'none');
if (versionCheckData.hasUpdate && !versionBannerDismissed) {
showUpdateBanner(versionCheckData);
addGlobalNotification(
@@ -299,3 +310,30 @@ function getVersionInfo() {
function isAutoUpdateEnabled() {
return autoUpdateEnabled;
}
/**
* Update version badge state
* @param {string} state - 'checking', 'has-update', 'none'
*/
function updateVersionBadge(state) {
const badge = document.getElementById('versionBadge');
if (!badge) return;
// Remove all state classes
badge.classList.remove('has-update', 'checking');
badge.textContent = '';
switch (state) {
case 'checking':
badge.classList.add('checking');
break;
case 'has-update':
badge.classList.add('has-update');
badge.textContent = '!';
break;
case 'none':
default:
// Hide badge
break;
}
}

View File

@@ -185,6 +185,14 @@ function renderIssueView() {
</div>
<div class="flex items-center gap-3">
<!-- Pull from GitHub Button -->
<button class="issue-pull-btn" onclick="showPullIssuesModal()" title="Pull issues from GitHub repository">
<svg class="w-4 h-4 mr-1.5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v 3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
<span>Pull from GitHub</span>
</button>
<!-- Create Button -->
<button class="issue-create-btn" onclick="showCreateIssueModal()">
<i data-lucide="plus" class="w-4 h-4"></i>
@@ -281,6 +289,59 @@ function renderIssueView() {
</div>
</div>
</div>
<!-- Pull Issues Modal -->
<div id="pullIssuesModal" class="issue-modal hidden">
<div class="issue-modal-backdrop" onclick="hidePullIssuesModal()"></div>
<div class="issue-modal-content">
<div class="issue-modal-header">
<h3>
<svg class="w-5 h-5 inline mr-2 -mt-1" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Pull Issues from GitHub
</h3>
<button class="btn-icon" onclick="hidePullIssuesModal()">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="issue-modal-body">
<div class="form-group">
<label>Issue State</label>
<select id="pullIssueState">
<option value="open" selected>Open</option>
<option value="closed">Closed</option>
<option value="all">All</option>
</select>
</div>
<div class="form-group">
<label>Maximum Issues</label>
<input type="number" id="pullIssueLimit" value="20" min="1" max="100" />
</div>
<div class="form-group">
<label>Labels (optional)</label>
<input type="text" id="pullIssueLabels" placeholder="bug, enhancement (comma-separated)" />
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="pullDownloadImages" checked />
<span>Download images to local</span>
</label>
<p class="form-hint text-xs text-muted-foreground mt-1">Images will be saved to .workflow/issues/images/ and links updated in issue context</p>
</div>
<div id="pullIssueResult" class="pull-result hidden mt-4 p-3 rounded-md bg-muted"></div>
</div>
<div class="issue-modal-footer">
<button class="btn-secondary" onclick="hidePullIssuesModal()">Cancel</button>
<button class="btn-primary" id="pullIssuesBtn" onclick="pullGitHubIssues()">
<svg class="w-4 h-4 mr-1 inline" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Pull Issues
</button>
</div>
</div>
</div>
</div>
`;
@@ -2627,6 +2688,127 @@ function hideCreateIssueModal() {
}
}
// ========== Pull Issues Modal ==========
function showPullIssuesModal() {
const modal = document.getElementById('pullIssuesModal');
if (modal) {
modal.classList.remove('hidden');
// Reset result area
const resultDiv = document.getElementById('pullIssueResult');
if (resultDiv) {
resultDiv.classList.add('hidden');
resultDiv.innerHTML = '';
}
lucide.createIcons();
}
}
function hidePullIssuesModal() {
const modal = document.getElementById('pullIssuesModal');
if (modal) {
modal.classList.add('hidden');
// Clear form
const stateSelect = document.getElementById('pullIssueState');
const limitInput = document.getElementById('pullIssueLimit');
const labelsInput = document.getElementById('pullIssueLabels');
const downloadImagesCheck = document.getElementById('pullDownloadImages');
if (stateSelect) stateSelect.value = 'open';
if (limitInput) limitInput.value = '20';
if (labelsInput) labelsInput.value = '';
if (downloadImagesCheck) downloadImagesCheck.checked = true;
}
}
async function pullGitHubIssues() {
const stateSelect = document.getElementById('pullIssueState');
const limitInput = document.getElementById('pullIssueLimit');
const labelsInput = document.getElementById('pullIssueLabels');
const downloadImagesCheck = document.getElementById('pullDownloadImages');
const resultDiv = document.getElementById('pullIssueResult');
const pullBtn = document.getElementById('pullIssuesBtn');
const state = stateSelect?.value || 'open';
const limit = parseInt(limitInput?.value || '20');
const labels = labelsInput?.value?.trim();
const downloadImages = downloadImagesCheck?.checked || false;
// Disable button and show loading
if (pullBtn) {
pullBtn.disabled = true;
pullBtn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 mr-1 animate-spin"></i>' + (t('common.loading') || 'Loading...');
lucide.createIcons();
}
try {
const params = new URLSearchParams({
path: projectPath,
state: state,
limit: limit.toString(),
downloadImages: downloadImages.toString()
});
if (labels) params.set('labels', labels);
const response = await fetch('/api/issues/pull?' + params.toString(), {
method: 'POST'
});
const result = await response.json();
if (!response.ok || result.error) {
showNotification(result.error || 'Failed to pull issues', 'error');
if (resultDiv) {
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = `<p class="text-destructive">${result.error || 'Failed to pull issues'}</p>`;
}
return;
}
// Show results
if (resultDiv) {
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = `
<div class="flex items-start gap-2">
<i data-lucide="check-circle" class="w-5 h-5 text-success mt-0.5"></i>
<div class="flex-1">
<p class="font-medium mb-2">${t('issues.pullSuccess') || 'GitHub Issues Pulled Successfully'}</p>
<div class="text-sm text-muted-foreground space-y-1">
<p>✓ Imported: <strong>${result.imported || 0}</strong> new issues</p>
<p>✓ Updated: <strong>${result.updated || 0}</strong> existing issues</p>
<p>✓ Skipped: <strong>${result.skipped || 0}</strong> unchanged issues</p>
${result.images_downloaded > 0 ? `<p>✓ Downloaded: <strong>${result.images_downloaded}</strong> images</p>` : ''}
</div>
</div>
</div>
`;
lucide.createIcons();
}
showNotification(`Pulled ${result.imported + result.updated} issues from GitHub`, 'success');
// Reload data after 1 second
setTimeout(async () => {
await loadIssueData();
renderIssueView();
hidePullIssuesModal();
}, 1500);
} catch (err) {
console.error('Failed to pull issues:', err);
showNotification('Failed to pull issues', 'error');
if (resultDiv) {
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = `<p class="text-destructive">${err.message || 'Unknown error occurred'}</p>`;
}
} finally {
// Re-enable button
if (pullBtn) {
pullBtn.disabled = false;
pullBtn.innerHTML = '<i data-lucide="download" class="w-4 h-4 mr-1"></i>' + (t('issues.pull') || 'Pull');
lucide.createIcons();
}
}
}
async function createIssue() {
const idInput = document.getElementById('newIssueId');
const titleInput = document.getElementById('newIssueTitle');

View File

@@ -1012,15 +1012,24 @@ async function saveTaskOrder(loopId, newOrder) {
/**
* Show add task modal
* Loads enabled tools before displaying modal to prevent race conditions
*/
async function showAddTaskModal(loopId) {
// Get enabled tools
const enabledTools = await getEnabledTools();
// Find and disable the "Add Task" button to prevent multiple clicks during loading
const addTaskButton = event?.target;
if (addTaskButton) {
addTaskButton.disabled = true;
const originalText = addTaskButton.innerHTML;
addTaskButton.innerHTML = '<i class="spinner"></i> ' + (t('common.loading') || 'Loading...');
// Build tool options HTML
const toolOptions = enabledTools.map(tool =>
`<option value="${tool}">${tool.charAt(0).toUpperCase() + tool.slice(1)}</option>`
).join('');
try {
// Get enabled tools (this ensures tools are loaded before modal opens)
const enabledTools = await getEnabledTools();
// Build tool options HTML
const toolOptions = enabledTools.map(tool =>
`<option value="${tool}">${tool.charAt(0).toUpperCase() + tool.slice(1)}</option>`
).join('');
const modal = document.createElement('div');
modal.id = 'addTaskModal';
@@ -1075,11 +1084,103 @@ async function showAddTaskModal(loopId) {
</div>
`;
document.body.appendChild(modal);
if (typeof lucide !== 'undefined') lucide.createIcons();
document.body.appendChild(modal);
if (typeof lucide !== 'undefined') lucide.createIcons();
// Focus on description field
setTimeout(() => document.getElementById('taskDescription').focus(), 100);
// Focus on description field
setTimeout(() => document.getElementById('taskDescription').focus(), 100);
} catch (err) {
console.error('Failed to show add task modal:', err);
alert(t('loop.loadToolsError') || 'Failed to load available tools. Please try again.');
} finally {
// Restore button state
if (addTaskButton) {
addTaskButton.disabled = false;
addTaskButton.innerHTML = originalText;
}
}
} else {
// Fallback if event is not available (shouldn't happen normally)
const enabledTools = await getEnabledTools();
const toolOptions = enabledTools.map(tool =>
`<option value="${tool}">${tool.charAt(0).toUpperCase() + tool.slice(1)}</option>`
).join('');
const modal = document.createElement('div');
modal.id = 'addTaskModal';
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3><i data-lucide="plus-circle" class="w-5 h-5"></i> ${t('loop.addTask') || 'Add Task'}</h3>
<button class="modal-close" onclick="closeTaskModal()">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="modal-body">
<form id="addTaskForm" onsubmit="handleAddTask(event, '${loopId}')">
<div id="addTaskError" class="alert alert-error" style="display: none;"></div>
<!-- Description -->
<div class="form-group">
<label for="taskDescription">${t('loop.taskDescription') || 'Task Description'} <span class="required">*</span></label>
<textarea id="taskDescription" name="description" rows="3" required
placeholder="${t('loop.taskDescriptionPlaceholder') || 'Describe what this task should do...'}"
class="form-control"></textarea>
</div>
<!-- Tool -->
<div class="form-group">
<label for="taskTool">${t('loop.tool') || 'Tool'}</label>
<select id="taskTool" name="tool" class="form-control">
${toolOptions}
</select>
</div>
<!-- Mode -->
<div class="form-group">
<label for="taskMode">${t('loop.mode') || 'Mode'}</label>
<select id="taskMode" name="mode" class="form-control">
<option value="analysis">${t('loop.modeAnalysis') || 'Analysis'}</option>
<option value="write">${t('loop.modeWrite') || 'Write'}</option>
<option value="review">${t('loop.modeReview') || 'Review'}</option>
</select>
</div>
<!-- Prompt Template -->
<div class="form-group">
<label for="taskPrompt">${t('loop.promptTemplate') || 'Prompt Template'} <span class="required">*</span></label>
<textarea id="taskPrompt" name="prompt_template" rows="5" required
placeholder="${t('loop.promptPlaceholder') || 'Enter the prompt to execute...'}"
class="form-control"></textarea>
<small class="form-help">${t('loop.promptHelp') || 'Variables: {iteration}, {output_prev}'}</small>
</div>
<!-- Error Handling -->
<div class="form-group">
<label for="taskOnError">${t('loop.onError') || 'On Error'}</label>
<select id="taskOnError" name="on_error" class="form-control">
<option value="continue">${t('loop.errorContinue') || 'Continue'}</option>
<option value="pause">${t('loop.errorPause') || 'Pause'}</option>
<option value="fail_fast">${t('loop.errorFailFast') || 'Fail Fast'}</option>
</select>
</div>
<!-- Form Actions -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeTaskModal()">${t('loop.cancel') || 'Cancel'}</button>
<button type="submit" class="btn btn-primary">${t('loop.add') || 'Add'}</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
if (typeof lucide !== 'undefined') lucide.createIcons();
setTimeout(() => document.getElementById('taskDescription').focus(), 100);
}
}
/**

View File

@@ -278,6 +278,50 @@
display: block;
animation: spin 1s linear infinite;
}
.check-icon-loading {
display: none;
}
/* Version Badge */
.version-badge {
display: none;
position: absolute;
top: -2px;
right: -2px;
min-width: 16px;
height: 16px;
padding: 0 4px;
background: hsl(var(--destructive));
color: white;
border-radius: 8px;
font-size: 0.625rem;
font-weight: 600;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 10;
}
.version-badge.has-update {
display: flex;
animation: badgePulse 2s ease-in-out infinite;
}
.version-badge.checking {
display: flex;
background: hsl(var(--muted) / 0.8);
}
.version-badge.checking::after {
content: '...';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes badgePulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
@keyframes dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
/* Auto-Update Toggle Switch */
.auto-update-switch {
@@ -390,7 +434,7 @@
<!-- Auto-Update Controls -->
<div class="flex items-center gap-2 border-l border-border pl-2">
<!-- Check Now Button -->
<button class="tooltip-bottom p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded" id="checkUpdateNow" data-tooltip="Check for updates now" onclick="checkForUpdatesNow()">
<button class="tooltip-bottom p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded relative" id="checkUpdateNow" data-tooltip="Check for updates now" onclick="checkForUpdatesNow()">
<!-- Download Icon (default) -->
<svg class="check-icon-default" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
@@ -398,9 +442,11 @@
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<!-- Loading Icon (checking state) -->
<svg class="check-icon-loading hidden" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="check-icon-loading" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
<!-- Version Available Badge -->
<span class="version-badge" id="versionBadge"></span>
</button>
<!-- Auto-Update Toggle Switch -->
<label class="tooltip-bottom auto-update-switch" data-tooltip="Auto-update">