feat(issue-management): Implement interactive issue management command with CRUD operations

- Added `/issue:manage` command for interactive issue management via CLI.
- Implemented features for listing, viewing, editing, deleting, and bulk operations on issues.
- Integrated GitHub issue fetching and text description parsing for issue creation.
- Enhanced user experience with menu-driven interface and structured output.
- Created helper functions for parsing user input and managing issue data.
- Added error handling and related command references for better usability.

feat(issue-creation): Introduce structured issue creation from GitHub URL or text description

- Added `/issue:new` command to create structured issues from GitHub issues or text descriptions.
- Implemented parsing logic for extracting key elements from issue descriptions.
- Integrated user confirmation for issue creation with options to edit title and priority.
- Ensured proper writing of issues to `.workflow/issues/issues.jsonl` with metadata.
- Included examples and error handling for various input scenarios.
This commit is contained in:
catlog22
2025-12-27 10:20:34 +08:00
parent 0157e36344
commit 8f310339df
9 changed files with 2359 additions and 37 deletions

View File

@@ -663,9 +663,11 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
for (const item of queue.queue) {
const statusColor = {
'pending': chalk.gray,
'ready': chalk.cyan,
'executing': chalk.yellow,
'completed': chalk.green,
'failed': chalk.red
'failed': chalk.red,
'blocked': chalk.magenta
}[item.status] || chalk.white;
console.log(

View File

@@ -21,6 +21,7 @@ const MODULE_FILES = [
'dashboard-js/components/tabs-other.js',
'dashboard-js/components/carousel.js',
'dashboard-js/components/notifications.js',
'dashboard-js/components/cli-stream-viewer.js',
'dashboard-js/components/global-notifications.js',
'dashboard-js/components/cli-status.js',
'dashboard-js/components/cli-history.js',

View File

@@ -88,7 +88,8 @@ const MODULE_CSS_FILES = [
'29-help.css',
'30-core-memory.css',
'31-api-settings.css',
'32-issue-manager.css'
'32-issue-manager.css',
'33-cli-stream-viewer.css'
];
// Modular JS files in dependency order
@@ -109,6 +110,7 @@ const MODULE_FILES = [
'components/flowchart.js',
'components/carousel.js',
'components/notifications.js',
'components/cli-stream-viewer.js',
'components/global-notifications.js',
'components/task-queue-sidebar.js',
'components/cli-status.js',

View File

@@ -16,6 +16,11 @@
color: hsl(var(--muted-foreground));
}
/* Issue Header */
.issue-header {
margin-bottom: 1.5rem;
}
/* View Toggle (Issues/Queue) */
.issue-view-toggle {
display: inline-flex;
@@ -60,17 +65,59 @@
width: 100%;
}
.issues-empty-state {
.issues-empty-state,
.issue-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 160px;
min-height: 200px;
text-align: center;
color: hsl(var(--muted-foreground));
}
.issue-empty svg {
margin-bottom: 1rem;
opacity: 0.5;
}
/* Issue Filters */
.issue-filters {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.issue-filter-btn {
padding: 0.375rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
background: hsl(var(--muted));
border: 1px solid transparent;
cursor: pointer;
transition: all 0.15s ease;
}
.issue-filter-btn:hover {
background: hsl(var(--muted) / 0.8);
color: hsl(var(--foreground));
}
.issue-filter-btn.active {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
/* Issue Card */
.issue-card {
position: relative;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
transition: all 0.2s ease;
cursor: pointer;
}
@@ -78,6 +125,7 @@
.issue-card:hover {
border-color: hsl(var(--primary));
transform: translateY(-2px);
box-shadow: 0 4px 12px hsl(var(--foreground) / 0.08);
}
.issue-card-header {
@@ -93,14 +141,16 @@
color: hsl(var(--muted-foreground));
}
.issue-card-title {
.issue-card-title,
.issue-title {
font-weight: 600;
font-size: 0.9375rem;
line-height: 1.4;
margin-top: 0.25rem;
}
.issue-card-meta {
.issue-card-meta,
.issue-meta {
display: flex;
align-items: center;
gap: 0.5rem;
@@ -141,6 +191,11 @@
color: hsl(var(--muted-foreground));
}
.issue-status.planning {
background: hsl(38 92% 50% / 0.15);
color: hsl(38 92% 50%);
}
.issue-status.planned {
background: hsl(217 91% 60% / 0.15);
color: hsl(217 91% 60%);
@@ -210,13 +265,30 @@
gap: 1.5rem;
}
.queue-empty-state {
.queue-empty-state,
.queue-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
text-align: center;
color: hsl(var(--muted-foreground));
}
.queue-empty svg {
margin-bottom: 1rem;
opacity: 0.5;
}
/* Issue ID */
.issue-id {
font-family: var(--font-mono);
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
background: hsl(var(--muted));
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
}
/* Execution Group */
@@ -977,3 +1049,713 @@
font-size: 0.75rem;
font-weight: 500;
}
/* ==========================================
TOOLBAR & SEARCH
========================================== */
.issue-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
padding: 0.75rem;
background: hsl(var(--muted) / 0.3);
border-radius: 0.5rem;
border: 1px solid hsl(var(--border));
}
.issue-search {
position: relative;
display: flex;
align-items: center;
flex: 1;
min-width: 200px;
max-width: 320px;
}
.issue-search > i:first-child {
position: absolute;
left: 0.75rem;
color: hsl(var(--muted-foreground));
pointer-events: none;
}
.issue-search input {
width: 100%;
padding: 0.5rem 2rem 0.5rem 2.25rem;
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
background: hsl(var(--background));
font-size: 0.875rem;
color: hsl(var(--foreground));
transition: border-color 0.15s ease;
}
.issue-search input:focus {
outline: none;
border-color: hsl(var(--primary));
}
.issue-search input::placeholder {
color: hsl(var(--muted-foreground));
}
.issue-search-clear {
position: absolute;
right: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border: none;
border-radius: 9999px;
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s ease;
}
.issue-search-clear:hover {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
/* ==========================================
CREATE BUTTON
========================================== */
.issue-create-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border: none;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.issue-create-btn:hover {
background: hsl(var(--primary) / 0.9);
transform: translateY(-1px);
}
.issue-create-btn:active {
transform: translateY(0);
}
/* ==========================================
ISSUE STATS
========================================== */
.issue-stats {
padding: 0.5rem 0;
}
/* ==========================================
EMPTY STATE (CENTERED)
========================================== */
.issue-empty-container {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.issue-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
}
.issue-empty > i,
.issue-empty > svg {
color: hsl(var(--muted-foreground));
opacity: 0.5;
margin-bottom: 1rem;
}
.issue-empty-title {
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
.issue-empty-hint {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
margin-bottom: 1.5rem;
}
.issue-empty-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.625rem 1.25rem;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border: none;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.issue-empty-btn:hover {
background: hsl(var(--primary) / 0.9);
}
/* ==========================================
MODAL STYLES
========================================== */
.issue-modal {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.issue-modal.hidden {
display: none;
}
.issue-modal-backdrop {
position: absolute;
inset: 0;
background: hsl(var(--foreground) / 0.5);
animation: fadeIn 0.15s ease-out;
}
.issue-modal-content {
position: relative;
width: 90%;
max-width: 480px;
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 0.75rem;
box-shadow: 0 20px 40px hsl(var(--foreground) / 0.15);
animation: modalSlideIn 0.2s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.issue-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid hsl(var(--border));
}
.issue-modal-header h3 {
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.issue-modal-body {
padding: 1.25rem;
max-height: 60vh;
overflow-y: auto;
}
.issue-modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.25rem;
border-top: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.3);
}
/* ==========================================
FORM STYLES
========================================== */
.form-group {
margin-bottom: 1rem;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group label {
display: block;
font-size: 0.8125rem;
font-weight: 500;
color: hsl(var(--foreground));
margin-bottom: 0.375rem;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.625rem 0.75rem;
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
background: hsl(var(--background));
font-size: 0.875rem;
color: hsl(var(--foreground));
transition: border-color 0.15s ease;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: hsl(var(--primary));
}
.form-group input::placeholder,
.form-group textarea::placeholder {
color: hsl(var(--muted-foreground));
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-group select {
cursor: pointer;
}
/* ==========================================
BUTTON STYLES
========================================== */
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border: none;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-primary:hover {
background: hsl(var(--primary) / 0.9);
}
.btn-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
background: hsl(var(--muted));
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-secondary:hover {
background: hsl(var(--muted) / 0.8);
}
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
background: transparent;
color: hsl(var(--muted-foreground));
border: none;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-icon:hover {
background: hsl(var(--muted));
color: hsl(var(--foreground));
}
/* ==========================================
DETAIL PANEL ENHANCEMENTS
========================================== */
.issue-detail-content {
padding: 1.25rem;
overflow-y: auto;
flex: 1;
}
.detail-section {
margin-bottom: 1.5rem;
}
.detail-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: hsl(var(--muted-foreground));
margin-bottom: 0.5rem;
}
.detail-editable {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.detail-value {
flex: 1;
font-size: 1rem;
font-weight: 500;
color: hsl(var(--foreground));
}
.detail-context {
position: relative;
}
.detail-pre {
font-family: inherit;
font-size: 0.875rem;
line-height: 1.6;
color: hsl(var(--foreground));
white-space: pre-wrap;
word-break: break-word;
padding: 0.75rem;
background: hsl(var(--muted) / 0.3);
border-radius: 0.375rem;
}
.btn-edit {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
padding: 0;
background: transparent;
color: hsl(var(--muted-foreground));
border: none;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-edit:hover {
background: hsl(var(--muted));
color: hsl(var(--primary));
}
/* Solutions List */
.solutions-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.solution-item {
padding: 0.75rem;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.15s ease;
}
.solution-item:hover {
border-color: hsl(var(--primary) / 0.5);
}
.solution-item.bound {
border-color: hsl(var(--success));
background: hsl(var(--success) / 0.05);
}
.solution-header {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.solution-id {
color: hsl(var(--muted-foreground));
}
.solution-bound-badge {
display: inline-flex;
padding: 0.125rem 0.375rem;
background: hsl(var(--success) / 0.15);
color: hsl(var(--success));
font-size: 0.6875rem;
font-weight: 500;
border-radius: 0.25rem;
}
.solution-tasks {
margin-left: auto;
color: hsl(var(--muted-foreground));
}
.solution-tasks-list {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid hsl(var(--border) / 0.5);
}
/* Tasks List */
.tasks-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.task-item-detail {
padding: 0.75rem;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
}
.task-title-detail {
font-size: 0.875rem;
color: hsl(var(--foreground));
margin-top: 0.375rem;
}
/* Edit Mode */
.edit-input,
.edit-textarea {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid hsl(var(--primary));
border-radius: 0.375rem;
background: hsl(var(--background));
font-size: 0.875rem;
color: hsl(var(--foreground));
}
.edit-input:focus,
.edit-textarea:focus {
outline: none;
}
.edit-textarea {
resize: vertical;
min-height: 100px;
}
.edit-actions {
display: flex;
gap: 0.25rem;
margin-top: 0.5rem;
}
.btn-save,
.btn-cancel {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
border: none;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-save {
background: hsl(var(--success) / 0.15);
color: hsl(var(--success));
}
.btn-save:hover {
background: hsl(var(--success) / 0.25);
}
.btn-cancel {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.btn-cancel:hover {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
/* ==========================================
QUEUE ENHANCEMENTS
========================================== */
.queue-info {
padding: 0.5rem 0;
}
.queue-items {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.queue-items.parallel {
flex-direction: row;
flex-wrap: wrap;
}
.queue-items.parallel .queue-item {
flex: 1;
min-width: 200px;
}
.queue-group-type {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
font-weight: 600;
}
.queue-group-type.parallel {
color: hsl(142 71% 45%);
}
.queue-group-type.sequential {
color: hsl(262 83% 58%);
}
/* Queue Item Status Colors */
.queue-item.ready {
border-color: hsl(199 89% 48%);
}
.queue-item.executing {
border-color: hsl(45 93% 47%);
background: hsl(45 93% 47% / 0.05);
}
.queue-item.completed {
border-color: hsl(var(--success));
background: hsl(var(--success) / 0.05);
}
.queue-item.failed {
border-color: hsl(var(--destructive));
background: hsl(var(--destructive) / 0.05);
}
.queue-item.blocked {
border-color: hsl(262 83% 58%);
opacity: 0.7;
}
/* Priority indicator */
.issue-priority {
display: inline-flex;
align-items: center;
gap: 0.125rem;
}
/* Conflicts list */
.conflicts-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.conflict-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
background: hsl(45 93% 47% / 0.1);
border: 1px solid hsl(45 93% 47% / 0.3);
border-radius: 0.375rem;
}
.conflict-file {
color: hsl(var(--primary));
}
.conflict-tasks {
flex: 1;
}
.conflict-status {
font-size: 0.6875rem;
font-weight: 500;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
}
.conflict-status.pending {
background: hsl(45 93% 47% / 0.15);
color: hsl(45 93% 47%);
}
.conflict-status.resolved {
background: hsl(var(--success) / 0.15);
color: hsl(var(--success));
}
/* ==========================================
RESPONSIVE TOOLBAR
========================================== */
@media (max-width: 640px) {
.issue-toolbar {
flex-direction: column;
align-items: stretch;
}
.issue-search {
max-width: none;
}
.issue-filters {
justify-content: flex-start;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}

View File

@@ -212,7 +212,7 @@ function renderStreamTabs() {
<span class="cli-stream-tab-mode">${exec.mode}</span>
<button class="cli-stream-tab-close ${canClose ? '' : 'disabled'}"
onclick="event.stopPropagation(); closeStream('${id}')"
title="${canClose ? t('cliStream.close') : t('cliStream.cannotCloseRunning')}"
title="${canClose ? _streamT('cliStream.close') : _streamT('cliStream.cannotCloseRunning')}"
${canClose ? '' : 'disabled'}>×</button>
</div>
`;
@@ -238,8 +238,8 @@ function renderStreamContent(executionId) {
contentContainer.innerHTML = `
<div class="cli-stream-empty">
<i data-lucide="terminal"></i>
<div class="cli-stream-empty-title" data-i18n="cliStream.noStreams">${t('cliStream.noStreams')}</div>
<div class="cli-stream-empty-hint" data-i18n="cliStream.noStreamsHint">${t('cliStream.noStreamsHint')}</div>
<div class="cli-stream-empty-title" data-i18n="cliStream.noStreams">${_streamT('cliStream.noStreams')}</div>
<div class="cli-stream-empty-hint" data-i18n="cliStream.noStreamsHint">${_streamT('cliStream.noStreamsHint')}</div>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
@@ -279,10 +279,10 @@ function renderStreamStatus(executionId) {
: formatDuration(Date.now() - exec.startTime);
const statusLabel = exec.status === 'running'
? t('cliStream.running')
? _streamT('cliStream.running')
: exec.status === 'completed'
? t('cliStream.completed')
: t('cliStream.error');
? _streamT('cliStream.completed')
: _streamT('cliStream.error');
statusContainer.innerHTML = `
<div class="cli-stream-status-info">
@@ -296,15 +296,15 @@ function renderStreamStatus(executionId) {
</div>
<div class="cli-stream-status-item">
<i data-lucide="file-text"></i>
<span>${exec.output.length} ${t('cliStream.lines') || 'lines'}</span>
<span>${exec.output.length} ${_streamT('cliStream.lines') || 'lines'}</span>
</div>
</div>
<div class="cli-stream-status-actions">
<button class="cli-stream-toggle-btn ${autoScrollEnabled ? 'active' : ''}"
onclick="toggleAutoScroll()"
title="${t('cliStream.autoScroll')}">
title="${_streamT('cliStream.autoScroll')}">
<i data-lucide="arrow-down-to-line"></i>
<span data-i18n="cliStream.autoScroll">${t('cliStream.autoScroll')}</span>
<span data-i18n="cliStream.autoScroll">${_streamT('cliStream.autoScroll')}</span>
</button>
</div>
`;
@@ -428,10 +428,15 @@ function escapeHtml(text) {
return div.innerHTML;
}
// Translation helper with fallback
function t(key) {
if (typeof window.t === 'function') {
return window.t(key);
// Translation helper with fallback (uses global t from i18n.js)
function _streamT(key) {
// First try global t() from i18n.js
if (typeof t === 'function' && t !== _streamT) {
try {
return t(key);
} catch (e) {
// Fall through to fallbacks
}
}
// Fallback values
const fallbacks = {

View File

@@ -1729,6 +1729,50 @@ const i18n = {
'nav.issues': 'Issues',
'nav.issueManager': 'Manager',
'title.issueManager': 'Issue Manager',
// issues.* keys (used by issue-manager.js)
'issues.title': 'Issue Manager',
'issues.description': 'Manage issues, solutions, and execution queue',
'issues.viewIssues': 'Issues',
'issues.viewQueue': 'Queue',
'issues.filterStatus': 'Status',
'issues.filterAll': 'All',
'issues.noIssues': 'No issues found',
'issues.createHint': 'Click "Create" to add your first issue',
'issues.priority': 'Priority',
'issues.tasks': 'tasks',
'issues.solutions': 'solutions',
'issues.boundSolution': 'Bound',
'issues.queueEmpty': 'Queue is empty',
'issues.reorderHint': 'Drag items within a group to reorder',
'issues.parallelGroup': 'Parallel',
'issues.sequentialGroup': 'Sequential',
'issues.dependsOn': 'Depends on',
// Create & Search
'issues.create': 'Create',
'issues.createTitle': 'Create New Issue',
'issues.issueId': 'Issue ID',
'issues.issueTitle': 'Title',
'issues.issueContext': 'Context',
'issues.issuePriority': 'Priority',
'issues.titlePlaceholder': 'Brief description of the issue',
'issues.contextPlaceholder': 'Detailed description, requirements, etc.',
'issues.priorityLowest': 'Lowest',
'issues.priorityLow': 'Low',
'issues.priorityMedium': 'Medium',
'issues.priorityHigh': 'High',
'issues.priorityCritical': 'Critical',
'issues.searchPlaceholder': 'Search issues...',
'issues.showing': 'Showing',
'issues.of': 'of',
'issues.issues': 'issues',
'issues.tryDifferentFilter': 'Try adjusting your search or filters',
'issues.createFirst': 'Create First Issue',
'issues.idRequired': 'Issue ID is required',
'issues.titleRequired': 'Title is required',
'issues.created': 'Issue created successfully',
'issues.confirmDelete': 'Are you sure you want to delete this issue?',
'issues.deleted': 'Issue deleted',
// issue.* keys (legacy)
'issue.viewIssues': 'Issues',
'issue.viewQueue': 'Queue',
'issue.filterAll': 'All',
@@ -3508,6 +3552,50 @@ const i18n = {
'nav.issues': '议题',
'nav.issueManager': '管理器',
'title.issueManager': '议题管理器',
// issues.* keys (used by issue-manager.js)
'issues.title': '议题管理器',
'issues.description': '管理议题、解决方案和执行队列',
'issues.viewIssues': '议题',
'issues.viewQueue': '队列',
'issues.filterStatus': '状态',
'issues.filterAll': '全部',
'issues.noIssues': '暂无议题',
'issues.createHint': '点击"创建"添加您的第一个议题',
'issues.priority': '优先级',
'issues.tasks': '任务',
'issues.solutions': '解决方案',
'issues.boundSolution': '已绑定',
'issues.queueEmpty': '队列为空',
'issues.reorderHint': '在组内拖拽项目以重新排序',
'issues.parallelGroup': '并行',
'issues.sequentialGroup': '顺序',
'issues.dependsOn': '依赖于',
// Create & Search
'issues.create': '创建',
'issues.createTitle': '创建新议题',
'issues.issueId': '议题ID',
'issues.issueTitle': '标题',
'issues.issueContext': '上下文',
'issues.issuePriority': '优先级',
'issues.titlePlaceholder': '简要描述议题',
'issues.contextPlaceholder': '详细描述、需求等',
'issues.priorityLowest': '最低',
'issues.priorityLow': '低',
'issues.priorityMedium': '中',
'issues.priorityHigh': '高',
'issues.priorityCritical': '紧急',
'issues.searchPlaceholder': '搜索议题...',
'issues.showing': '显示',
'issues.of': '共',
'issues.issues': '条议题',
'issues.tryDifferentFilter': '尝试调整搜索或筛选条件',
'issues.createFirst': '创建第一个议题',
'issues.idRequired': '议题ID为必填',
'issues.titleRequired': '标题为必填',
'issues.created': '议题创建成功',
'issues.confirmDelete': '确定要删除此议题吗?',
'issues.deleted': '议题已删除',
// issue.* keys (legacy)
'issue.viewIssues': '议题',
'issue.viewQueue': '队列',
'issue.filterAll': '全部',

View File

@@ -10,6 +10,7 @@ var issueData = {
selectedIssue: null,
selectedSolution: null,
statusFilter: 'all',
searchQuery: '',
viewMode: 'issues' // 'issues' | 'queue'
};
var issueLoading = false;
@@ -91,10 +92,20 @@ function renderIssueView() {
if (!container) return;
const issues = issueData.issues || [];
const filteredIssues = issueData.statusFilter === 'all'
// Apply both status and search filters
let filteredIssues = issueData.statusFilter === 'all'
? issues
: issues.filter(i => i.status === issueData.statusFilter);
if (issueData.searchQuery) {
const query = issueData.searchQuery.toLowerCase();
filteredIssues = filteredIssues.filter(i =>
i.id.toLowerCase().includes(query) ||
(i.title && i.title.toLowerCase().includes(query)) ||
(i.context && i.context.toLowerCase().includes(query))
);
}
container.innerHTML = `
<div class="issue-manager">
<!-- Header -->
@@ -110,16 +121,24 @@ function renderIssueView() {
</div>
</div>
<!-- View Toggle -->
<div class="issue-view-toggle">
<button class="${issueData.viewMode === 'issues' ? 'active' : ''}" onclick="switchIssueView('issues')">
<i data-lucide="list" class="w-4 h-4 mr-1"></i>
${t('issues.viewIssues') || 'Issues'}
</button>
<button class="${issueData.viewMode === 'queue' ? 'active' : ''}" onclick="switchIssueView('queue')">
<i data-lucide="git-branch" class="w-4 h-4 mr-1"></i>
${t('issues.viewQueue') || 'Queue'}
<div class="flex items-center gap-3">
<!-- Create Button -->
<button class="issue-create-btn" onclick="showCreateIssueModal()">
<i data-lucide="plus" class="w-4 h-4"></i>
<span>${t('issues.create') || 'Create'}</span>
</button>
<!-- View Toggle -->
<div class="issue-view-toggle">
<button class="${issueData.viewMode === 'issues' ? 'active' : ''}" onclick="switchIssueView('issues')">
<i data-lucide="list" class="w-4 h-4 mr-1"></i>
${t('issues.viewIssues') || 'Issues'}
</button>
<button class="${issueData.viewMode === 'queue' ? 'active' : ''}" onclick="switchIssueView('queue')">
<i data-lucide="git-branch" class="w-4 h-4 mr-1"></i>
${t('issues.viewQueue') || 'Queue'}
</button>
</div>
</div>
</div>
</div>
@@ -128,6 +147,47 @@ function renderIssueView() {
<!-- Detail Panel -->
<div id="issueDetailPanel" class="issue-detail-panel hidden"></div>
<!-- Create Issue Modal -->
<div id="createIssueModal" class="issue-modal hidden">
<div class="issue-modal-backdrop" onclick="hideCreateIssueModal()"></div>
<div class="issue-modal-content">
<div class="issue-modal-header">
<h3>${t('issues.createTitle') || 'Create New Issue'}</h3>
<button class="btn-icon" onclick="hideCreateIssueModal()">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="issue-modal-body">
<div class="form-group">
<label>${t('issues.issueId') || 'Issue ID'}</label>
<input type="text" id="newIssueId" placeholder="e.g., GH-123 or TASK-001" />
</div>
<div class="form-group">
<label>${t('issues.issueTitle') || 'Title'}</label>
<input type="text" id="newIssueTitle" placeholder="${t('issues.titlePlaceholder') || 'Brief description of the issue'}" />
</div>
<div class="form-group">
<label>${t('issues.issueContext') || 'Context'} (${t('common.optional') || 'optional'})</label>
<textarea id="newIssueContext" rows="4" placeholder="${t('issues.contextPlaceholder') || 'Detailed description, requirements, etc.'}"></textarea>
</div>
<div class="form-group">
<label>${t('issues.issuePriority') || 'Priority'}</label>
<select id="newIssuePriority">
<option value="1">1 - ${t('issues.priorityLowest') || 'Lowest'}</option>
<option value="2">2 - ${t('issues.priorityLow') || 'Low'}</option>
<option value="3" selected>3 - ${t('issues.priorityMedium') || 'Medium'}</option>
<option value="4">4 - ${t('issues.priorityHigh') || 'High'}</option>
<option value="5">5 - ${t('issues.priorityCritical') || 'Critical'}</option>
</select>
</div>
</div>
<div class="issue-modal-footer">
<button class="btn-secondary" onclick="hideCreateIssueModal()">${t('common.cancel') || 'Cancel'}</button>
<button class="btn-primary" onclick="createIssue()">${t('issues.create') || 'Create'}</button>
</div>
</div>
</div>
</div>
`;
@@ -147,11 +207,26 @@ function switchIssueView(mode) {
// ========== Issue List Section ==========
function renderIssueListSection(issues) {
const statuses = ['all', 'registered', 'planning', 'planned', 'queued', 'executing', 'completed', 'failed'];
const totalIssues = issueData.issues?.length || 0;
return `
<!-- Filters -->
<div class="issue-filters mb-4">
<div class="flex items-center gap-2 flex-wrap">
<!-- Toolbar: Search + Filters -->
<div class="issue-toolbar mb-4">
<div class="issue-search">
<i data-lucide="search" class="w-4 h-4"></i>
<input type="text"
id="issueSearchInput"
placeholder="${t('issues.searchPlaceholder') || 'Search issues...'}"
value="${issueData.searchQuery}"
oninput="handleIssueSearch(this.value)" />
${issueData.searchQuery ? `
<button class="issue-search-clear" onclick="clearIssueSearch()">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
` : ''}
</div>
<div class="issue-filters">
<span class="text-sm text-muted-foreground">${t('issues.filterStatus') || 'Status'}:</span>
${statuses.map(status => `
<button class="issue-filter-btn ${issueData.statusFilter === status ? 'active' : ''}"
@@ -162,13 +237,30 @@ function renderIssueListSection(issues) {
</div>
</div>
<!-- Issues Stats -->
<div class="issue-stats mb-4">
<span class="text-sm text-muted-foreground">
${t('issues.showing') || 'Showing'} <strong>${issues.length}</strong> ${t('issues.of') || 'of'} <strong>${totalIssues}</strong> ${t('issues.issues') || 'issues'}
</span>
</div>
<!-- Issues Grid -->
<div class="issues-grid">
${issues.length === 0 ? `
<div class="issue-empty">
<i data-lucide="inbox" class="w-12 h-12 text-muted-foreground mb-4"></i>
<p class="text-muted-foreground">${t('issues.noIssues') || 'No issues found'}</p>
<p class="text-sm text-muted-foreground mt-2">${t('issues.createHint') || 'Create issues using: ccw issue init <id>'}</p>
<div class="issue-empty-container">
<div class="issue-empty">
<i data-lucide="inbox" class="w-16 h-16"></i>
<p class="issue-empty-title">${t('issues.noIssues') || 'No issues found'}</p>
<p class="issue-empty-hint">${issueData.searchQuery || issueData.statusFilter !== 'all'
? (t('issues.tryDifferentFilter') || 'Try adjusting your search or filters')
: (t('issues.createHint') || 'Click "Create" to add your first issue')}</p>
${!issueData.searchQuery && issueData.statusFilter === 'all' ? `
<button class="issue-empty-btn" onclick="showCreateIssueModal()">
<i data-lucide="plus" class="w-4 h-4"></i>
${t('issues.createFirst') || 'Create First Issue'}
</button>
` : ''}
</div>
</div>
` : issues.map(issue => renderIssueCard(issue)).join('')}
</div>
@@ -702,3 +794,122 @@ async function updateTaskStatus(issueId, taskId, status) {
showNotification('Failed to update task status', 'error');
}
}
// ========== Search Functions ==========
function handleIssueSearch(value) {
issueData.searchQuery = value;
renderIssueView();
}
function clearIssueSearch() {
issueData.searchQuery = '';
renderIssueView();
}
// ========== Create Issue Modal ==========
function showCreateIssueModal() {
const modal = document.getElementById('createIssueModal');
if (modal) {
modal.classList.remove('hidden');
lucide.createIcons();
// Focus on first input
setTimeout(() => {
document.getElementById('newIssueId')?.focus();
}, 100);
}
}
function hideCreateIssueModal() {
const modal = document.getElementById('createIssueModal');
if (modal) {
modal.classList.add('hidden');
// Clear form
const idInput = document.getElementById('newIssueId');
const titleInput = document.getElementById('newIssueTitle');
const contextInput = document.getElementById('newIssueContext');
const prioritySelect = document.getElementById('newIssuePriority');
if (idInput) idInput.value = '';
if (titleInput) titleInput.value = '';
if (contextInput) contextInput.value = '';
if (prioritySelect) prioritySelect.value = '3';
}
}
async function createIssue() {
const idInput = document.getElementById('newIssueId');
const titleInput = document.getElementById('newIssueTitle');
const contextInput = document.getElementById('newIssueContext');
const prioritySelect = document.getElementById('newIssuePriority');
const issueId = idInput?.value?.trim();
const title = titleInput?.value?.trim();
const context = contextInput?.value?.trim();
const priority = parseInt(prioritySelect?.value || '3');
if (!issueId) {
showNotification(t('issues.idRequired') || 'Issue ID is required', 'error');
idInput?.focus();
return;
}
if (!title) {
showNotification(t('issues.titleRequired') || 'Title is required', 'error');
titleInput?.focus();
return;
}
try {
const response = await fetch('/api/issues?path=' + encodeURIComponent(projectPath), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: issueId,
title: title,
context: context,
priority: priority,
source: 'dashboard'
})
});
const result = await response.json();
if (!response.ok || result.error) {
showNotification(result.error || 'Failed to create issue', 'error');
return;
}
showNotification(t('issues.created') || 'Issue created successfully', 'success');
hideCreateIssueModal();
// Reload data and refresh view
await loadIssueData();
renderIssueView();
} catch (err) {
console.error('Failed to create issue:', err);
showNotification('Failed to create issue', 'error');
}
}
// ========== Delete Issue ==========
async function deleteIssue(issueId) {
if (!confirm(t('issues.confirmDelete') || 'Are you sure you want to delete this issue?')) {
return;
}
try {
const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete');
showNotification(t('issues.deleted') || 'Issue deleted', 'success');
closeIssueDetail();
// Reload data and refresh view
await loadIssueData();
renderIssueView();
} catch (err) {
showNotification('Failed to delete issue', 'error');
}
}