From 13c4dd0032551a3abcab4eddea556afd3bbb3052 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 7 Feb 2026 22:50:36 +0800 Subject: [PATCH] feat: enhance issue management with edit functionality and UI improvements - Added Edit Issue dialog to allow users to modify existing issues with fields for title, context, priority, and status. - Integrated form validation and state management for the edit dialog. - Updated the IssueManagerPage to handle opening and closing of the edit dialog, as well as submitting updates. - Improved UI components in SolutionDrawer and ExplorationSection for better user experience. - Refactored file path input with a browse dialog for selecting files and directories in SettingsPage. - Adjusted layout and styling in LeftSidebar and OrchestratorPage for better responsiveness and usability. - Updated localization files to include new strings for the edit dialog and task management. --- .../workflow-lite-plan-execute/SKILL.md | 211 ++++++------------ .../phases/01-lite-plan.md | 20 +- .../phases/04-lite-execute.md | 4 +- .../components/issue/queue/SolutionDrawer.tsx | 95 ++++++-- .../context/ExplorationCollapsible.tsx | 2 +- .../context/ExplorationsSection.tsx | 158 +++++++++---- ccw/frontend/src/locales/en/issues.json | 21 +- ccw/frontend/src/locales/zh/issues.json | 21 +- ccw/frontend/src/pages/IssueManagerPage.tsx | 159 ++++++++++++- ccw/frontend/src/pages/SettingsPage.tsx | 158 ++++++++++++- .../src/pages/orchestrator/LeftSidebar.tsx | 14 +- .../pages/orchestrator/OrchestratorPage.tsx | 24 +- ccw/frontend/src/stores/flowStore.ts | 4 +- 13 files changed, 636 insertions(+), 255 deletions(-) diff --git a/.codex/skills/workflow-lite-plan-execute/SKILL.md b/.codex/skills/workflow-lite-plan-execute/SKILL.md index cef47baa..ef847227 100644 --- a/.codex/skills/workflow-lite-plan-execute/SKILL.md +++ b/.codex/skills/workflow-lite-plan-execute/SKILL.md @@ -1,149 +1,55 @@ --- name: workflow-lite-plan-execute -description: Lightweight planning and execution skill. Exploration → Clarification → Planning → Confirmation → Execution via lite-execute. +description: Lightweight planning + execution workflow. Exploration -> Clarification -> Planning -> Confirmation -> Execution (via lite-execute). allowed-tools: spawn_agent, wait, send_input, close_agent, AskUserQuestion, Read, Write, Edit, Bash, Glob, Grep, mcp__ace-tool__search_context --- # Planning Workflow -Lightweight planning skill: Lite Plan produces an implementation plan, then hands off to Lite Execute for task execution. - -## Architecture Overview - -``` -┌──────────────────────────────────────────────────┐ -│ Planning Workflow Orchestrator (SKILL.md) │ -│ → Parse args → Lite Plan → Lite Execute │ -└────────────┬─────────────────────────────────────┘ - │ - ┌────────┴────────┐ - ↓ ↓ -┌────────┐ ┌────────────┐ -│Phase 1 │────→│ Phase 4 │ -│ Lite │ │ Lite │ -│ Plan │ │ Execute │ -└────────┘ └────────────┘ -``` +Lite Plan produces an implementation plan and an `executionContext`, then hands off to Lite Execute for task execution. ## Key Design Principles -1. **Shared Execution**: Lite Plan produces `executionContext` consumed by Phase 4 (lite-execute) +1. **Shared Execution**: Lite Plan produces `executionContext` consumed by Phase 4 (Lite Execute) 2. **Progressive Phase Loading**: Only load phase docs when about to execute -3. **Auto-Continue**: Planning completes → automatically loads execution phase -4. **Default Auto Mode**: When `--yes`, skip confirmations and auto-approve plan +3. **Auto-Continue**: After the plan is confirmed ("Allow"), automatically load execution phase +4. **Default Auto Mode**: When `--yes`, skip confirmations and auto-approve the plan ## Auto Mode -When `--yes` or `-y`: Auto-approve plan, skip clarifications, use default execution settings. +When `--yes` or `-y`: +- Auto-approve plan and use default execution settings +- Skip non-critical clarifications; still ask minimal clarifications if required for safety/correctness -## Usage +## Usage (Pseudo) -```bash +This section describes the skill input shape; actual invocation depends on the host runtime. + +``` $workflow-lite-plan-execute -$workflow-lite-plan-execute [FLAGS] "" +$workflow-lite-plan-execute [FLAGS] "" # Flags --y, --yes Skip all confirmations (auto mode) +-y, --yes Skip confirmations (auto mode) -e, --explore Force exploration phase +``` -# Examples +Examples: +``` $workflow-lite-plan-execute "Implement JWT authentication" $workflow-lite-plan-execute -y "Add user profile page" $workflow-lite-plan-execute -e "Refactor payment module" +$workflow-lite-plan-execute "docs/todo.md" ``` > **Implementation sketch**: 编排器内部使用 `Skill(skill="workflow-lite-plan-execute", args="...")` 接口调用,此为伪代码示意,非命令行语法。 -## Subagent API Reference - -### spawn_agent - -Create a new subagent with task assignment. - -```javascript -const agentId = spawn_agent({ - message: ` -## TASK ASSIGNMENT - -### MANDATORY FIRST STEPS (Agent Execute) -1. **Read role definition**: ~/.codex/agents/{agent-type}.md (MUST read first) -2. Read: .workflow/project-tech.json -3. Read: .workflow/project-guidelines.json - -## TASK CONTEXT -${taskContext} - -## DELIVERABLES -${deliverables} -` -}) -``` - -### wait - -Get results from subagent (only way to retrieve results). - -```javascript -const result = wait({ - ids: [agentId], - timeout_ms: 600000 // 10 minutes -}) - -if (result.timed_out) { - // Handle timeout - can continue waiting or send_input to prompt completion -} -``` - -### send_input - -Continue interaction with active subagent (for clarification or follow-up). - -```javascript -send_input({ - id: agentId, - message: ` -## CLARIFICATION ANSWERS -${answers} - -## NEXT STEP -Continue with plan generation. -` -}) -``` - -### close_agent - -Clean up subagent resources (irreversible). - -```javascript -close_agent({ id: agentId }) -``` - -## Execution Flow - -``` -Input Parsing: - ├─ Extract flags: --yes, --explore - └─ Extract task description (string or file path) - -Planning Phase: - └─ Phase 1: Lite Plan - └─ Ref: phases/01-lite-plan.md - └─ Output: executionContext (plan.json + explorations + selections) - -Execution Phase: - └─ Phase 4: Lite Execute - └─ Ref: phases/04-lite-execute.md - └─ Input: executionContext from planning phase - └─ Output: Executed tasks + optional code review -``` - -**Phase Reference Documents** (read on-demand when phase executes): +## Phase Reference Documents (Read On Demand) | Phase | Document | Purpose | |-------|----------|---------| -| 1 | [phases/01-lite-plan.md](phases/01-lite-plan.md) | Lightweight planning with exploration, clarification, and plan generation | -| 4 | [phases/04-lite-execute.md](phases/04-lite-execute.md) | Shared execution engine: task grouping, batch execution, code review | +| 1 | `phases/01-lite-plan.md` | Lightweight planning with exploration, clarification, plan generation, and confirmation | +| 4 | `phases/04-lite-execute.md` | Shared execution engine: task grouping, batch execution, optional code review | ## Orchestrator Logic @@ -151,44 +57,56 @@ Execution Phase: // Flag parsing const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y') const forceExplore = $ARGUMENTS.includes('--explore') || $ARGUMENTS.includes('-e') + +// Task extraction rule: +// - Strip known flags: -y/--yes, -e/--explore +// - Remaining args are joined as the task description +// - Treat it as a file path ONLY if (a) exactly one arg remains AND (b) the path exists +function extractTaskDescription(args) { + const knownFlags = new Set(['--yes', '-y', '--explore', '-e']) + const rest = args.filter(a => !knownFlags.has(a)) + if (rest.length === 1 && file_exists(rest[0])) return rest[0] + return rest.join(' ').trim() +} + const taskDescription = extractTaskDescription($ARGUMENTS) // Phase 1: Lite Plan Read('phases/01-lite-plan.md') // Execute planning phase... -// After planning completes: +// Gate: only continue when confirmed (or --yes) +if (executionContext?.userSelection?.confirmation !== 'Allow' && !autoYes) { + // Stop: user cancelled or requested modifications + return +} + +// Phase 4: Lite Execute Read('phases/04-lite-execute.md') // Execute execution phase with executionContext from Phase 1 ``` -## Data Flow +## executionContext Contract (High Level) +`executionContext` is the only contract between Phase 1 and Phase 4. + +Required (minimum) fields: +```javascript +{ + planObject: { summary, approach, tasks, complexity, estimated_time, recommended_execution }, + originalUserInput: string, + executionMethod: "Agent" | "Codex" | "Auto", + codeReviewTool: "Skip" | "Gemini Review" | "Codex Review" | "Agent Review" | string, + userSelection: { confirmation: "Allow" | "Modify" | "Cancel" } +} ``` -Phase 1: Lite Plan - │ - ├─ Produces: executionContext = { - │ planObject: plan.json, - │ explorationsContext, - │ clarificationContext, - │ executionMethod: "Agent" | "Codex" | "Auto", - │ codeReviewTool: "Skip" | "Gemini Review" | ..., - │ originalUserInput: string, - │ session: { id, folder, artifacts } - │ } - │ - ↓ -Phase 4: Lite Execute - │ - ├─ Consumes: executionContext - ├─ Task grouping → Batch creation → Parallel/sequential execution - ├─ Optional code review - └─ Development index update -``` + +Recommended fields: +- `explorationsContext`, `clarificationContext`, `executorAssignments`, and `session` (artifacts folder + paths) ## TodoWrite Pattern -**Initialization**: +Initialization: ```json [ {"content": "Lite Plan - Planning", "status": "in_progress", "activeForm": "Planning"}, @@ -196,7 +114,7 @@ Phase 4: Lite Execute ] ``` -**After planning completes**: +After planning completes: ```json [ {"content": "Lite Plan - Planning", "status": "completed", "activeForm": "Planning"}, @@ -204,15 +122,14 @@ Phase 4: Lite Execute ] ``` -Phase-internal sub-tasks are managed by each phase document (attach/collapse pattern). - ## Core Rules -1. **Planning phase NEVER executes code** - all execution delegated to Phase 4 -2. **Phase 4 ALWAYS runs** after planning completes +1. **Planning phase NEVER modifies project code** - it may write planning artifacts, but all implementation is delegated to Phase 4 +2. **Phase 4 runs only after confirmation** - execute only when confirmation is "Allow" (or `--yes` auto mode) 3. **executionContext is the contract** between planning and execution phases -4. **Progressive loading**: Read phase doc ONLY when about to execute -5. **Explicit Lifecycle**: Always close_agent after wait completes to free resources +4. **Progressive loading**: Read phase doc only when about to execute +5. **File-path detection**: Treat input as a file path only if the path exists; do not infer from file extensions +6. **Explicit lifecycle**: Always `close_agent` after `wait` completes ## Error Handling @@ -224,5 +141,5 @@ Phase-internal sub-tasks are managed by each phase document (attach/collapse pat ## Related Skills -- Full planning workflow: [workflow-plan-execute/SKILL.md](../workflow-plan-execute/SKILL.md) -- Brainstorming: [workflow-brainstorm-auto-parallel/SKILL.md](../workflow-brainstorm-auto-parallel/SKILL.md) +- Full planning workflow: `../workflow-plan-execute/SKILL.md` +- Brainstorming: `../workflow-brainstorm-auto-parallel/SKILL.md` diff --git a/.codex/skills/workflow-lite-plan-execute/phases/01-lite-plan.md b/.codex/skills/workflow-lite-plan-execute/phases/01-lite-plan.md index b4b80644..a4a7cb13 100644 --- a/.codex/skills/workflow-lite-plan-execute/phases/01-lite-plan.md +++ b/.codex/skills/workflow-lite-plan-execute/phases/01-lite-plan.md @@ -971,13 +971,12 @@ if (autoYes) { userSelection = { confirmation: "Allow", - execution_method: "Auto", - code_review_tool: "Skip" + executionMethod: "Auto", + codeReviewTool: "Skip" } } else { // Interactive mode: Ask user - // Note: Execution "Other" option allows specifying CLI tools from ~/.claude/cli-tools.json - userSelection = ASK_USER([ + const rawSelection = ASK_USER([ { id: "confirm", type: "select", @@ -1013,6 +1012,12 @@ if (autoYes) { default: "Skip" } ]) // BLOCKS (wait for user response) + + userSelection = { + confirmation: rawSelection.confirm, + executionMethod: rawSelection.execution, + codeReviewTool: rawSelection.review + } } ``` @@ -1043,11 +1048,12 @@ executionContext = { explorationAngles: manifest.explorations.map(e => e.angle), explorationManifest: manifest, clarificationContext: clarificationContext || null, - executionMethod: userSelection.execution_method, // 全局默认,可被 executorAssignments 覆盖 - codeReviewTool: userSelection.code_review_tool, + userSelection: userSelection, + executionMethod: userSelection.executionMethod, // Global default; may be overridden by executorAssignments + codeReviewTool: userSelection.codeReviewTool, originalUserInput: task_description, - // 任务级 executor 分配(优先于全局 executionMethod) + // Task-level executor assignments (priority over global executionMethod) executorAssignments: executorAssignments, // { taskId: { executor, reason } } session: { diff --git a/.codex/skills/workflow-lite-plan-execute/phases/04-lite-execute.md b/.codex/skills/workflow-lite-plan-execute/phases/04-lite-execute.md index f9856861..21ac091a 100644 --- a/.codex/skills/workflow-lite-plan-execute/phases/04-lite-execute.md +++ b/.codex/skills/workflow-lite-plan-execute/phases/04-lite-execute.md @@ -146,10 +146,10 @@ If `isPlanJson === false`: Input Parsing: └─ Decision (mode detection): ├─ --in-memory flag → Mode 1: Load executionContext → Skip user selection - ├─ Ends with .md/.json/.txt → Mode 3: Read file → Detect format + ├─ Existing file path (path exists) → Mode 3: Read file → Detect format │ ├─ Valid plan.json → Use planObject → User selects method + review │ └─ Not plan.json → Treat as prompt → User selects method + review - └─ Other → Mode 2: Prompt description → User selects method + review + └─ Otherwise → Mode 2: Prompt description → User selects method + review Execution: ├─ Step 1: Initialize result tracking (previousExecutionResults = []) diff --git a/ccw/frontend/src/components/issue/queue/SolutionDrawer.tsx b/ccw/frontend/src/components/issue/queue/SolutionDrawer.tsx index 3ecfb03e..2e1c4ab3 100644 --- a/ccw/frontend/src/components/issue/queue/SolutionDrawer.tsx +++ b/ccw/frontend/src/components/issue/queue/SolutionDrawer.tsx @@ -3,12 +3,13 @@ // ======================================== // Right-side solution detail drawer -import { useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { X, FileText, CheckCircle, Circle, Loader2, XCircle, Clock, AlertTriangle } from 'lucide-react'; import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/Button'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs'; +import { useIssueQueue } from '@/hooks'; import { cn } from '@/lib/utils'; import type { QueueItem } from '@/lib/api'; @@ -36,17 +37,34 @@ const statusConfig: Record('overview'); + const { data: queue } = useIssueQueue(); + const itemId = item?.item_id; + const solutionId = item?.solution_id; // ESC key to close - useState(() => { + useEffect(() => { + if (!isOpen) return; const handleEsc = (e: KeyboardEvent) => { - if (e.key === 'Escape' && isOpen) { - onClose(); - } + if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); - }); + }, [isOpen, onClose]); + + // Reset tab when switching items + useEffect(() => { + if (!isOpen || !itemId) return; + setActiveTab('overview'); + }, [itemId, isOpen]); + + const tasksForSolution = useMemo(() => { + if (!solutionId) return []; + const allItems = Object.values(queue?.grouped_items || {}).flat(); + const isTaskItem = (qi: QueueItem) => Boolean(qi.task_id) || qi.item_id.startsWith('task-'); + return allItems + .filter((qi) => qi.solution_id === solutionId && isTaskItem(qi)) + .sort((a, b) => a.execution_order - b.execution_order); + }, [queue?.grouped_items, solutionId]); if (!item || !isOpen) { return null; @@ -56,7 +74,6 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) { const StatusIcon = status.icon; // Get solution details (would need to fetch full solution data) - const solutionId = item.solution_id; const issueId = item.issue_id; return ( @@ -93,10 +110,10 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) {

- {formatMessage({ id: 'solution.issue' })}: {issueId} + {formatMessage({ id: 'issues.solution.issue' })}: {issueId}

- {formatMessage({ id: 'solution.solution' })}: {solutionId} + {formatMessage({ id: 'issues.solution.solution' })}: {solutionId}

@@ -111,15 +128,15 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) { - {formatMessage({ id: 'solution.tabs.overview' })} + {formatMessage({ id: 'issues.solution.tabs.overview' })} - {formatMessage({ id: 'solution.tabs.tasks' })} + {formatMessage({ id: 'issues.solution.tabs.tasks' })} - {formatMessage({ id: 'solution.tabs.json' })} + {formatMessage({ id: 'issues.solution.tabs.json' })} @@ -131,23 +148,23 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) { {/* Execution Info */}

- {formatMessage({ id: 'solution.overview.executionInfo' })} + {formatMessage({ id: 'issues.solution.overview.executionInfo' })}

-

{formatMessage({ id: 'solution.overview.executionOrder' })}

+

{formatMessage({ id: 'issues.solution.overview.executionOrder' })}

{item.execution_order}

-

{formatMessage({ id: 'solution.overview.semanticPriority' })}

+

{formatMessage({ id: 'issues.solution.overview.semanticPriority' })}

{item.semantic_priority}

-

{formatMessage({ id: 'solution.overview.group' })}

+

{formatMessage({ id: 'issues.solution.overview.group' })}

{item.execution_group}

-

{formatMessage({ id: 'solution.overview.taskCount' })}

+

{formatMessage({ id: 'issues.solution.overview.taskCount' })}

{item.task_count || '-'}

@@ -157,7 +174,7 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) { {item.depends_on && item.depends_on.length > 0 && (

- {formatMessage({ id: 'solution.overview.dependencies' })} + {formatMessage({ id: 'issues.solution.overview.dependencies' })}

{item.depends_on.map((dep, index) => ( @@ -173,7 +190,7 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) { {item.files_touched && item.files_touched.length > 0 && (

- {formatMessage({ id: 'solution.overview.filesTouched' })} + {formatMessage({ id: 'issues.solution.overview.filesTouched' })}

{item.files_touched.map((file, index) => ( @@ -189,10 +206,42 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) { {/* Tasks Tab */} -
- -

{formatMessage({ id: 'solution.tasks.comingSoon' })}

-
+ {tasksForSolution.length === 0 ? ( +
+ +

{formatMessage({ id: 'issues.solution.tasks.empty' })}

+
+ ) : ( +
+ {tasksForSolution.map((task) => { + const taskStatus = statusConfig[task.status] || statusConfig.pending; + const TaskStatusIcon = taskStatus.icon; + const taskId = task.task_id || task.item_id; + + return ( +
+
+

{taskId}

+

+ {formatMessage({ id: 'issues.solution.overview.executionOrder' })}: {task.execution_order} + {' · '} + {formatMessage({ id: 'issues.solution.overview.group' })}: {task.execution_group} +

+
+ + + {formatMessage({ id: taskStatus.label })} + +
+ ); + })} +
+ )}
{/* JSON Tab */} diff --git a/ccw/frontend/src/components/session-detail/context/ExplorationCollapsible.tsx b/ccw/frontend/src/components/session-detail/context/ExplorationCollapsible.tsx index d901c9fa..70bf7c70 100644 --- a/ccw/frontend/src/components/session-detail/context/ExplorationCollapsible.tsx +++ b/ccw/frontend/src/components/session-detail/context/ExplorationCollapsible.tsx @@ -37,7 +37,7 @@ export function ExplorationCollapsible({
{icon} - {title} + {title}
- - - + +

+ {formatMessage({ id: 'sessionDetail.context.explorations.title' })} - - ({data.manifest.exploration_count} {formatMessage({ id: 'sessionDetail.context.explorations.angles' })}) - - - - + + {data.manifest.exploration_count} {formatMessage({ id: 'sessionDetail.context.explorations.angles' })} + +

{explorationEntries.map(([angle, angleData]) => ( 0) return [val]; + return []; +} + function AngleContent({ data }: AngleContentProps) { const { formatMessage } = useIntl(); @@ -95,60 +106,73 @@ function AngleContent({ data }: AngleContentProps) { key: string; icon: JSX.Element; label: string; - data: unknown; + items: string[]; + renderAs: 'paths' | 'text'; }> = []; - if (data.project_structure && data.project_structure.length > 0) { + const projectStructure = toStringArray(data.project_structure); + if (projectStructure.length > 0) { sections.push({ key: 'project_structure', icon: , label: formatMessage({ id: 'sessionDetail.context.explorations.projectStructure' }), - data: data.project_structure, + items: projectStructure, + renderAs: 'paths', }); } - if (data.relevant_files && data.relevant_files.length > 0) { + const relevantFiles = toStringArray(data.relevant_files); + if (relevantFiles.length > 0) { sections.push({ key: 'relevant_files', icon: , label: formatMessage({ id: 'sessionDetail.context.explorations.relevantFiles' }), - data: data.relevant_files, + items: relevantFiles, + renderAs: 'paths', }); } - if (data.patterns && data.patterns.length > 0) { + const patterns = toStringArray(data.patterns); + if (patterns.length > 0) { sections.push({ key: 'patterns', icon: , label: formatMessage({ id: 'sessionDetail.context.explorations.patterns' }), - data: data.patterns, + items: patterns, + renderAs: 'text', }); } - if (data.dependencies && data.dependencies.length > 0) { + const dependencies = toStringArray(data.dependencies); + if (dependencies.length > 0) { sections.push({ key: 'dependencies', icon: , label: formatMessage({ id: 'sessionDetail.context.explorations.dependencies' }), - data: data.dependencies, + items: dependencies, + renderAs: 'text', }); } - if (data.integration_points && data.integration_points.length > 0) { + const integrationPoints = toStringArray(data.integration_points); + if (integrationPoints.length > 0) { sections.push({ key: 'integration_points', icon: , label: formatMessage({ id: 'sessionDetail.context.explorations.integrationPoints' }), - data: data.integration_points, + items: integrationPoints, + renderAs: 'text', }); } - if (data.testing && data.testing.length > 0) { + const testing = toStringArray(data.testing); + if (testing.length > 0) { sections.push({ key: 'testing', icon: , label: formatMessage({ id: 'sessionDetail.context.explorations.testing' }), - data: data.testing, + items: testing, + renderAs: 'text', }); } @@ -157,22 +181,78 @@ function AngleContent({ data }: AngleContentProps) { } return ( -
+
{sections.map((section) => ( -
- {section.icon} -
-

- {section.label} -

- -
+
+

+ {section.icon} + {section.label} +

+ {section.renderAs === 'paths' ? ( +
+ {section.items.map((item, i) => ( +
+ + {item} +
+ ))} +
+ ) : ( +
    + {section.items.map((item, i) => ( +
  • + + + + +
  • + ))} +
+ )}
))}
); } +/** Render text with inline code/path highlighting */ +function FormattedTextItem({ text }: { text: string }) { + // Split on backtick-wrapped or path-like segments + const parts = text.split(/(`[^`]+`)/g); + if (parts.length === 1) { + // No backtick segments, check for embedded paths + const pathParts = text.split(/(\S+\/\S+\.\w+)/g); + if (pathParts.length === 1) return <>{text}; + return ( + <> + {pathParts.map((part, i) => + isPathLike(part) ? ( + {part} + ) : ( + {part} + ) + )} + + ); + } + return ( + <> + {parts.map((part, i) => + part.startsWith('`') && part.endsWith('`') ? ( + + {part.slice(1, -1)} + + ) : ( + {part} + ) + )} + + ); +} + function formatAngleTitle(angle: string): string { return angle .replace(/_/g, ' ') diff --git a/ccw/frontend/src/locales/en/issues.json b/ccw/frontend/src/locales/en/issues.json index 3c16de61..5f1a2268 100644 --- a/ccw/frontend/src/locales/en/issues.json +++ b/ccw/frontend/src/locales/en/issues.json @@ -55,6 +55,24 @@ "creating": "Creating..." } }, + "editDialog": { + "title": "Edit Issue", + "labels": { + "title": "Title", + "context": "Context", + "priority": "Priority", + "status": "Status" + }, + "placeholders": { + "title": "Enter issue title...", + "context": "Describe the issue context..." + }, + "buttons": { + "cancel": "Cancel", + "save": "Save", + "saving": "Saving..." + } + }, "card": { "id": "ID", "createdAt": "Created", @@ -179,7 +197,8 @@ "filesTouched": "Files Touched" }, "tasks": { - "comingSoon": "Task list coming soon" + "comingSoon": "Task list coming soon", + "empty": "No tasks found for this solution" } }, "discovery": { diff --git a/ccw/frontend/src/locales/zh/issues.json b/ccw/frontend/src/locales/zh/issues.json index f1381ed9..423f706a 100644 --- a/ccw/frontend/src/locales/zh/issues.json +++ b/ccw/frontend/src/locales/zh/issues.json @@ -55,6 +55,24 @@ "creating": "创建中..." } }, + "editDialog": { + "title": "编辑问题", + "labels": { + "title": "标题", + "context": "上下文", + "priority": "优先级", + "status": "状态" + }, + "placeholders": { + "title": "输入问题标题...", + "context": "描述问题上下文..." + }, + "buttons": { + "cancel": "取消", + "save": "保存", + "saving": "保存中..." + } + }, "card": { "id": "ID", "createdAt": "创建时间", @@ -183,7 +201,8 @@ "filesTouched": "涉及文件" }, "tasks": { - "comingSoon": "任务列表即将推出" + "comingSoon": "任务列表即将推出", + "empty": "该解决方案下暂无任务" } }, "discovery": { diff --git a/ccw/frontend/src/pages/IssueManagerPage.tsx b/ccw/frontend/src/pages/IssueManagerPage.tsx index d3843bf3..b8f2b661 100644 --- a/ccw/frontend/src/pages/IssueManagerPage.tsx +++ b/ccw/frontend/src/pages/IssueManagerPage.tsx @@ -3,7 +3,7 @@ // ======================================== // Track and manage project issues with drag-drop queue -import { useState, useMemo } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useIntl } from 'react-intl'; import { AlertCircle, @@ -121,6 +121,132 @@ function NewIssueDialog({ open, onOpenChange, onSubmit, isCreating }: NewIssueDi ); } +// ========== Edit Issue Dialog ========== + +interface EditIssueDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + issue: Issue | null; + onSubmit: (issueId: string, data: { title: string; context?: string; priority: Issue['priority']; status: Issue['status'] }) => void; + isUpdating: boolean; +} + +function EditIssueDialog({ open, onOpenChange, issue, onSubmit, isUpdating }: EditIssueDialogProps) { + const { formatMessage } = useIntl(); + const [title, setTitle] = useState(''); + const [context, setContext] = useState(''); + const [priority, setPriority] = useState('medium'); + const [status, setStatus] = useState('open'); + + // Reset form when dialog opens or issue changes + useEffect(() => { + if (open && issue) { + setTitle(issue.title ?? ''); + setContext(issue.context ?? ''); + setPriority(issue.priority ?? 'medium'); + setStatus(issue.status ?? 'open'); + } else if (!open) { + setTitle(''); + setContext(''); + setPriority('medium'); + setStatus('open'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, issue?.id]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!issue) return; + if (!title.trim()) return; + + onSubmit(issue.id, { + title: title.trim(), + context: context.trim() || undefined, + priority, + status, + }); + }; + + return ( + + + + {formatMessage({ id: 'issues.editDialog.title' })} + +
+
+ + setTitle(e.target.value)} + placeholder={formatMessage({ id: 'issues.editDialog.placeholders.title' })} + className="mt-1" + required + /> +
+
+ +