Add Multi-CLI Plan feature and corresponding JSON schema

- Introduced a new navigation item for "Multi-CLI Plan" in the dashboard template.
- Created a new JSON schema for "Multi-CLI Discussion Artifact" to facilitate structured discussions and decision-making processes.
This commit is contained in:
catlog22
2026-01-13 23:46:15 +08:00
parent c3da637849
commit 6922ca27de
12 changed files with 2535 additions and 274 deletions

View File

@@ -45,220 +45,19 @@ You are a multi-CLI collaborative discussion agent. You orchestrate multiple CLI
Write to: `{session.folder}/rounds/{round_number}/synthesis.json`
### Core Types
```typescript
/** Multi-language label for UI display */
interface I18nLabel {
en: string;
zh: string;
}
/** Discussion status */
type Status = 'exploring' | 'analyzing' | 'debating' | 'decided' | 'blocked';
/** Priority/Impact levels */
type Level = 'critical' | 'high' | 'medium' | 'low';
/** Decision reversibility */
type Reversibility = 'easily_reversible' | 'requires_refactoring' | 'irreversible';
/** Agent identifier */
interface AgentIdentifier {
name: 'Gemini' | 'Codex' | 'Qwen' | 'Human';
id: string;
}
**Schema Reference**: Load schema before generating output:
```bash
cat ~/.claude/workflows/cli-templates/schemas/multi-cli-discussion-schema.json
```
### Main Artifact Structure
```typescript
interface DiscussionArtifact {
metadata: ArtifactMetadata;
discussionTopic: DiscussionTopicSection;
relatedFiles: RelatedFilesSection;
planning: PlanningRequirementsSection;
decision: DecisionSection;
decisionRecords: DecisionRecordsSection;
// Internal analysis data (for debugging/auditing)
_internal: {
cli_analyses: CLIAnalysis[];
cross_verification: CrossVerification;
convergence: ConvergenceMetrics;
};
}
```
### Section 1: Metadata
```typescript
interface ArtifactMetadata {
artifactId: string; // e.g., "MCP-auth-refactor-2026-01-13-round-1"
roundId: number;
timestamp: string; // ISO 8601
contributingAgents: AgentIdentifier[];
durationSeconds: number;
exportFormats: ('markdown' | 'html')[];
}
```
### Section 2: Discussion Topic (讨论主题)
```typescript
interface DiscussionTopicSection {
title: I18nLabel;
description: I18nLabel;
scope: {
included: I18nLabel[]; // What's in scope
excluded: I18nLabel[]; // What's explicitly out of scope
};
keyQuestions: I18nLabel[]; // Questions being explored
status: Status;
tags: string[]; // For filtering: ["auth", "security", "api"]
}
```
### Section 3: Related Files (关联文件)
```typescript
interface RelatedFilesSection {
fileTree: FileNode[];
dependencyGraph: DependencyEdge[];
impactSummary: FileImpact[];
}
interface FileNode {
path: string;
type: 'file' | 'directory';
modificationStatus: 'added' | 'modified' | 'deleted' | 'unchanged';
impactScore?: Level;
children?: FileNode[];
codeSnippet?: CodeSnippet;
}
interface DependencyEdge {
source: string; // File path
target: string; // File path
relationship: string; // 'imports' | 'calls' | 'inherits' | 'uses'
}
interface FileImpact {
filePath: string;
line?: number;
score: Level;
reasoning: I18nLabel;
}
interface CodeSnippet {
startLine: number;
endLine: number;
code: string;
language: string;
comment?: I18nLabel;
}
```
### Section 4: Planning Requirements (规划要求)
```typescript
interface PlanningRequirementsSection {
functional: Requirement[];
nonFunctional: Requirement[];
acceptanceCriteria: AcceptanceCriterion[];
}
interface Requirement {
id: string; // e.g., "FR-01", "NFR-01"
description: I18nLabel;
priority: Level;
source: string; // "User Request", "Technical Debt", etc.
}
interface AcceptanceCriterion {
id: string; // e.g., "AC-01"
description: I18nLabel;
isMet: boolean;
}
```
### Section 5: Decision (决策)
```typescript
interface DecisionSection {
status: 'pending' | 'decided' | 'conflict';
summary: I18nLabel;
selectedSolution?: Solution;
rejectedAlternatives: RejectedSolution[];
confidenceScore: number; // 0.0 to 1.0
}
interface Solution {
id: string; // e.g., "sol-jwt-01"
title: I18nLabel;
description: I18nLabel;
pros: I18nLabel[];
cons: I18nLabel[];
estimatedEffort: I18nLabel; // e.g., "3 developer-days"
risk: Level;
affectedFiles: FileImpact[];
sourceCLIs: string[]; // Which CLIs proposed this
}
interface RejectedSolution extends Solution {
rejectionReason: I18nLabel;
}
```
### Section 6: Decision Records (决策记录)
```typescript
interface DecisionRecordsSection {
timeline: DecisionEvent[];
}
interface DecisionEvent {
eventId: string; // e.g., "evt-proposal-001"
timestamp: string; // ISO 8601
type: 'proposal' | 'argument' | 'agreement' | 'disagreement' | 'decision' | 'reversal';
contributor: AgentIdentifier;
summary: I18nLabel;
evidence: Evidence[];
reversibility?: Reversibility;
}
interface Evidence {
type: 'link' | 'code_snippet' | 'log_output' | 'benchmark' | 'reference';
content: string | CodeSnippet;
description: I18nLabel;
}
```
### Internal Analysis Data
```typescript
interface CLIAnalysis {
tool: 'gemini' | 'codex' | 'qwen';
perspective: string;
feasibility_score: number;
findings: string[];
implementation_approaches: ImplementationApproach[];
technical_concerns: string[];
code_locations: FileImpact[];
}
interface CrossVerification {
agreements: string[];
disagreements: string[];
resolution: string;
}
interface ConvergenceMetrics {
score: number;
new_insights: boolean;
recommendation: 'continue' | 'converged' | 'user_input_needed';
}
```
**Main Sections**:
- `metadata`: Artifact ID, round, timestamp, contributing agents
- `discussionTopic`: Title, description, scope (included/excluded), key questions, status, tags
- `relatedFiles`: File tree, dependency graph, impact summary
- `planning`: Functional requirements, non-functional requirements, acceptance criteria
- `decision`: Status, summary, selected solution, rejected alternatives, confidence score
- `decisionRecords`: Timeline of decision events (proposals, agreements, disagreements)
- `_internal`: CLI analyses, cross-verification results, convergence metrics
## Execution Flow
@@ -1118,46 +917,3 @@ function validateAnalysis(analysis) {
- Generate more than 4 clarification questions
- Ignore previous round context
## UI Component Mapping
For dashboard visualization, map artifact sections to UI components:
| Section | Component | Library Example | Notes |
|---------|-----------|-----------------|-------|
| **metadata** | | | |
| `roundId`, `timestamp` | `Tag`, `Badge` | Ant Design `Tag` | Header indicators |
| `contributingAgents` | `Avatar.Group` | Ant Design `Avatar.Group` | Agent icons with tooltips |
| `exportFormats` | `Dropdown` + `Button` | Material-UI `Menu` | Export actions |
| **discussionTopic** | `Card` | Bootstrap `Card` | Main section container |
| `title`, `description` | `Typography` | Any UI library | Standard text |
| `scope` | `List` with icons | Heroicons | Included/Excluded lists |
| `keyQuestions` | `Collapse` | Ant Design `Collapse` | Expandable Q&A |
| `status` | `Steps`, `Timeline` | Ant Design `Steps` | Progress indicator |
| **relatedFiles** | | | |
| `fileTree` | `Tree` | Ant Design `Tree` | Hierarchical file view |
| `dependencyGraph` | `Graph` | `vis-network`, `react-flow` | Interactive graph |
| `impactSummary` | `Table` | Ant Design `Table` | Sortable impact list |
| `codeSnippet` | `SyntaxHighlighter` | `react-syntax-highlighter` | Code with line numbers |
| **planning** | `Tabs` | Bootstrap `Navs` | FR/NFR/AC tabs |
| `functional/nonFunctional` | `Table` | Material-UI `Table` | Priority-sortable |
| `acceptanceCriteria` | `List` + `Checkbox` | Ant Design `List` | Checkable items |
| `priority` | `Tag` (color-coded) | Ant Design `Tag` | critical=red, high=orange |
| **decision** | | | |
| `summary` | `Alert`, `Callout` | Ant Design `Alert` | Prominent decision box |
| `selectedSolution` | `Card` (highlighted) | Bootstrap `Card` | Winner card |
| `rejectedAlternatives` | `Collapse` of `Card`s | Ant Design `Collapse` | Collapsed alternatives |
| `pros/cons` | `List` with icons | ThumbUp/ThumbDown | Visual indicators |
| `confidenceScore` | `Progress`, `Gauge` | Ant Design `Progress` | 0-100% visual |
| **decisionRecords** | | | |
| `timeline` | `Timeline` | Ant Design `Timeline`, `react-chrono` | Chronological events |
| `contributor` | `Avatar` + `Tooltip` | Ant Design `Avatar` | Who contributed |
| `evidence` | `Popover`, `Modal` | Ant Design `Popover` | Click to expand |
| `reversibility` | `Tag` with icon | SyncOutlined | Reversibility indicator |
### Visualization Recommendations
1. **Real-time Updates**: Use WebSocket or SSE for live synthesis.json updates
2. **Responsive Layout**: Card grid → stacked on mobile
3. **Dark/Light Theme**: CSS variables for theme switching
4. **Export**: Generate Markdown via template, HTML via React-to-static
5. **i18n Toggle**: Language switch button in header, read `en`/`zh` from I18nLabel

View File

@@ -0,0 +1,421 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Multi-CLI Discussion Artifact Schema",
"description": "Visualization-friendly output for multi-CLI collaborative discussion agent",
"type": "object",
"required": ["metadata", "discussionTopic", "relatedFiles", "planning", "decision", "decisionRecords"],
"properties": {
"metadata": {
"type": "object",
"required": ["artifactId", "roundId", "timestamp", "contributingAgents"],
"properties": {
"artifactId": {
"type": "string",
"description": "Unique ID for this artifact (e.g., 'MCP-auth-refactor-2026-01-13-round-1')"
},
"roundId": {
"type": "integer",
"minimum": 1,
"description": "Discussion round number"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp"
},
"contributingAgents": {
"type": "array",
"items": {
"$ref": "#/definitions/AgentIdentifier"
},
"description": "Agents that contributed to this artifact"
},
"durationSeconds": {
"type": "integer",
"description": "Total duration in seconds"
},
"exportFormats": {
"type": "array",
"items": {
"type": "string",
"enum": ["markdown", "html"]
},
"description": "Supported export formats"
}
}
},
"discussionTopic": {
"type": "object",
"required": ["title", "description", "status"],
"properties": {
"title": {
"$ref": "#/definitions/I18nLabel"
},
"description": {
"$ref": "#/definitions/I18nLabel"
},
"scope": {
"type": "object",
"properties": {
"included": {
"type": "array",
"items": { "$ref": "#/definitions/I18nLabel" },
"description": "What's in scope"
},
"excluded": {
"type": "array",
"items": { "$ref": "#/definitions/I18nLabel" },
"description": "What's explicitly out of scope"
}
}
},
"keyQuestions": {
"type": "array",
"items": { "$ref": "#/definitions/I18nLabel" },
"description": "Questions being explored"
},
"status": {
"type": "string",
"enum": ["exploring", "analyzing", "debating", "decided", "blocked"],
"description": "Discussion status"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Tags for filtering (e.g., ['auth', 'security', 'api'])"
}
}
},
"relatedFiles": {
"type": "object",
"properties": {
"fileTree": {
"type": "array",
"items": { "$ref": "#/definitions/FileNode" },
"description": "File tree structure"
},
"dependencyGraph": {
"type": "array",
"items": { "$ref": "#/definitions/DependencyEdge" },
"description": "Dependency relationships"
},
"impactSummary": {
"type": "array",
"items": { "$ref": "#/definitions/FileImpact" },
"description": "File impact summary"
}
}
},
"planning": {
"type": "object",
"properties": {
"functional": {
"type": "array",
"items": { "$ref": "#/definitions/Requirement" },
"description": "Functional requirements"
},
"nonFunctional": {
"type": "array",
"items": { "$ref": "#/definitions/Requirement" },
"description": "Non-functional requirements"
},
"acceptanceCriteria": {
"type": "array",
"items": { "$ref": "#/definitions/AcceptanceCriterion" },
"description": "Acceptance criteria"
}
}
},
"decision": {
"type": "object",
"required": ["status", "confidenceScore"],
"properties": {
"status": {
"type": "string",
"enum": ["pending", "decided", "conflict"],
"description": "Decision status"
},
"summary": {
"$ref": "#/definitions/I18nLabel"
},
"selectedSolution": {
"$ref": "#/definitions/Solution"
},
"rejectedAlternatives": {
"type": "array",
"items": { "$ref": "#/definitions/RejectedSolution" }
},
"confidenceScore": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Confidence score (0.0 to 1.0)"
}
}
},
"decisionRecords": {
"type": "object",
"properties": {
"timeline": {
"type": "array",
"items": { "$ref": "#/definitions/DecisionEvent" },
"description": "Timeline of decision events"
}
}
},
"_internal": {
"type": "object",
"description": "Internal analysis data (for debugging)",
"properties": {
"cli_analyses": {
"type": "array",
"items": { "$ref": "#/definitions/CLIAnalysis" }
},
"cross_verification": {
"$ref": "#/definitions/CrossVerification"
},
"convergence": {
"$ref": "#/definitions/ConvergenceMetrics"
}
}
}
},
"definitions": {
"I18nLabel": {
"type": "object",
"required": ["en", "zh"],
"properties": {
"en": { "type": "string" },
"zh": { "type": "string" }
},
"description": "Multi-language label for UI display"
},
"AgentIdentifier": {
"type": "object",
"required": ["name", "id"],
"properties": {
"name": {
"type": "string",
"enum": ["Gemini", "Codex", "Qwen", "Human", "System"]
},
"id": { "type": "string" }
}
},
"FileNode": {
"type": "object",
"required": ["path", "type"],
"properties": {
"path": { "type": "string" },
"type": {
"type": "string",
"enum": ["file", "directory"]
},
"modificationStatus": {
"type": "string",
"enum": ["added", "modified", "deleted", "unchanged"]
},
"impactScore": {
"type": "string",
"enum": ["critical", "high", "medium", "low"]
},
"children": {
"type": "array",
"items": { "$ref": "#/definitions/FileNode" }
},
"codeSnippet": { "$ref": "#/definitions/CodeSnippet" }
}
},
"DependencyEdge": {
"type": "object",
"required": ["source", "target", "relationship"],
"properties": {
"source": { "type": "string" },
"target": { "type": "string" },
"relationship": { "type": "string" }
}
},
"FileImpact": {
"type": "object",
"required": ["filePath", "score", "reasoning"],
"properties": {
"filePath": { "type": "string" },
"line": { "type": "integer" },
"score": {
"type": "string",
"enum": ["critical", "high", "medium", "low"]
},
"reasoning": { "$ref": "#/definitions/I18nLabel" }
}
},
"CodeSnippet": {
"type": "object",
"required": ["startLine", "endLine", "code"],
"properties": {
"startLine": { "type": "integer" },
"endLine": { "type": "integer" },
"code": { "type": "string" },
"language": { "type": "string" },
"comment": { "$ref": "#/definitions/I18nLabel" }
}
},
"Requirement": {
"type": "object",
"required": ["id", "description", "priority"],
"properties": {
"id": { "type": "string" },
"description": { "$ref": "#/definitions/I18nLabel" },
"priority": {
"type": "string",
"enum": ["critical", "high", "medium", "low"]
},
"source": { "type": "string" }
}
},
"AcceptanceCriterion": {
"type": "object",
"required": ["id", "description", "isMet"],
"properties": {
"id": { "type": "string" },
"description": { "$ref": "#/definitions/I18nLabel" },
"isMet": { "type": "boolean" }
}
},
"Solution": {
"type": "object",
"required": ["id", "title", "description"],
"properties": {
"id": { "type": "string" },
"title": { "$ref": "#/definitions/I18nLabel" },
"description": { "$ref": "#/definitions/I18nLabel" },
"pros": {
"type": "array",
"items": { "$ref": "#/definitions/I18nLabel" }
},
"cons": {
"type": "array",
"items": { "$ref": "#/definitions/I18nLabel" }
},
"estimatedEffort": { "$ref": "#/definitions/I18nLabel" },
"risk": {
"type": "string",
"enum": ["critical", "high", "medium", "low"]
},
"affectedFiles": {
"type": "array",
"items": { "$ref": "#/definitions/FileImpact" }
},
"sourceCLIs": {
"type": "array",
"items": { "type": "string" }
}
}
},
"RejectedSolution": {
"allOf": [
{ "$ref": "#/definitions/Solution" },
{
"type": "object",
"required": ["rejectionReason"],
"properties": {
"rejectionReason": { "$ref": "#/definitions/I18nLabel" }
}
}
]
},
"DecisionEvent": {
"type": "object",
"required": ["eventId", "timestamp", "type", "contributor", "summary"],
"properties": {
"eventId": { "type": "string" },
"timestamp": {
"type": "string",
"format": "date-time"
},
"type": {
"type": "string",
"enum": ["proposal", "argument", "agreement", "disagreement", "decision", "reversal"]
},
"contributor": { "$ref": "#/definitions/AgentIdentifier" },
"summary": { "$ref": "#/definitions/I18nLabel" },
"evidence": {
"type": "array",
"items": { "$ref": "#/definitions/Evidence" }
},
"reversibility": {
"type": "string",
"enum": ["easily_reversible", "requires_refactoring", "irreversible"]
}
}
},
"Evidence": {
"type": "object",
"required": ["type", "content", "description"],
"properties": {
"type": {
"type": "string",
"enum": ["link", "code_snippet", "log_output", "benchmark", "reference"]
},
"content": {},
"description": { "$ref": "#/definitions/I18nLabel" }
}
},
"CLIAnalysis": {
"type": "object",
"required": ["tool", "perspective", "feasibility_score"],
"properties": {
"tool": {
"type": "string",
"enum": ["gemini", "codex", "qwen"]
},
"perspective": { "type": "string" },
"feasibility_score": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"findings": {
"type": "array",
"items": { "type": "string" }
},
"implementation_approaches": { "type": "array" },
"technical_concerns": {
"type": "array",
"items": { "type": "string" }
},
"code_locations": {
"type": "array",
"items": { "$ref": "#/definitions/FileImpact" }
}
}
},
"CrossVerification": {
"type": "object",
"properties": {
"agreements": {
"type": "array",
"items": { "type": "string" }
},
"disagreements": {
"type": "array",
"items": { "type": "string" }
},
"resolution": { "type": "string" }
}
},
"ConvergenceMetrics": {
"type": "object",
"properties": {
"score": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"new_insights": { "type": "boolean" },
"recommendation": {
"type": "string",
"enum": ["continue", "converged", "user_input_needed"]
}
}
}
}
}

View File

@@ -63,6 +63,7 @@ interface LiteSession {
interface LiteTasks {
litePlan: LiteSession[];
liteFix: LiteSession[];
multiCliPlan: LiteSession[];
}
interface LiteTaskDetail {
@@ -84,13 +85,15 @@ interface LiteTaskDetail {
export async function scanLiteTasks(workflowDir: string): Promise<LiteTasks> {
const litePlanDir = join(workflowDir, '.lite-plan');
const liteFixDir = join(workflowDir, '.lite-fix');
const multiCliDir = join(workflowDir, '.multi-cli-plan');
const [litePlan, liteFix] = await Promise.all([
const [litePlan, liteFix, multiCliPlan] = await Promise.all([
scanLiteDir(litePlanDir, 'lite-plan'),
scanLiteDir(liteFixDir, 'lite-fix'),
scanMultiCliDir(multiCliDir),
]);
return { litePlan, liteFix };
return { litePlan, liteFix, multiCliPlan };
}
/**
@@ -142,6 +145,141 @@ async function scanLiteDir(dir: string, type: string): Promise<LiteSession[]> {
}
}
/**
* Scan multi-cli-plan directory for sessions
* @param dir - Directory path to .multi-cli-plan
* @returns Array of multi-cli sessions
*/
async function scanMultiCliDir(dir: string): Promise<LiteSession[]> {
try {
const entries = await readdir(dir, { withFileTypes: true });
const sessions = (await Promise.all(
entries
.filter((entry) => entry.isDirectory())
.map(async (entry) => {
const sessionPath = join(dir, entry.name);
const [createdAt, syntheses] = await Promise.all([
getCreatedTime(sessionPath),
loadRoundSyntheses(sessionPath),
]);
// Extract plan from latest synthesis if available
const latestSynthesis = syntheses.length > 0 ? syntheses[syntheses.length - 1] : null;
// Calculate progress based on round count and convergence
const progress = calculateMultiCliProgress(syntheses);
const session: LiteSession = {
id: entry.name,
type: 'multi-cli-plan',
path: sessionPath,
createdAt,
plan: latestSynthesis,
tasks: extractTasksFromSyntheses(syntheses),
progress,
};
return session;
}),
))
.filter((session): session is LiteSession => session !== null)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
return sessions;
} catch (err: any) {
if (err?.code === 'ENOENT') return [];
console.error(`Error scanning ${dir}:`, err?.message || String(err));
return [];
}
}
interface RoundSynthesis {
round: number;
converged?: boolean;
tasks?: unknown[];
synthesis?: unknown;
[key: string]: unknown;
}
/**
* Load all synthesis.json files from rounds subdirectories
* @param sessionPath - Session directory path
* @returns Array of synthesis objects sorted by round number
*/
async function loadRoundSyntheses(sessionPath: string): Promise<RoundSynthesis[]> {
const roundsDir = join(sessionPath, 'rounds');
const syntheses: RoundSynthesis[] = [];
try {
const roundEntries = await readdir(roundsDir, { withFileTypes: true });
const roundDirs = roundEntries
.filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name))
.map((entry) => ({
name: entry.name,
num: parseInt(entry.name, 10),
}))
.sort((a, b) => a.num - b.num);
for (const roundDir of roundDirs) {
const synthesisPath = join(roundsDir, roundDir.name, 'synthesis.json');
try {
const content = await readFile(synthesisPath, 'utf8');
const synthesis = JSON.parse(content) as RoundSynthesis;
synthesis.round = roundDir.num;
syntheses.push(synthesis);
} catch {
// Skip if synthesis.json doesn't exist or can't be parsed
}
}
} catch {
// Return empty array if rounds directory doesn't exist
}
return syntheses;
}
/**
* Calculate progress for multi-cli-plan sessions
* @param syntheses - Array of round syntheses
* @returns Progress info
*/
function calculateMultiCliProgress(syntheses: RoundSynthesis[]): Progress {
if (syntheses.length === 0) {
return { total: 0, completed: 0, percentage: 0 };
}
const latestSynthesis = syntheses[syntheses.length - 1];
const isConverged = latestSynthesis.converged === true;
// Total is based on expected rounds or actual rounds
const total = syntheses.length;
const completed = isConverged ? total : Math.max(0, total - 1);
const percentage = isConverged ? 100 : Math.round((completed / Math.max(total, 1)) * 100);
return { total, completed, percentage };
}
/**
* Extract tasks from synthesis objects
* @param syntheses - Array of round syntheses
* @returns Normalized tasks from latest synthesis
*/
function extractTasksFromSyntheses(syntheses: RoundSynthesis[]): NormalizedTask[] {
if (syntheses.length === 0) return [];
const latestSynthesis = syntheses[syntheses.length - 1];
const tasks = latestSynthesis.tasks;
if (!Array.isArray(tasks)) return [];
return tasks
.map((task) => normalizeTask(task))
.filter((task): task is NormalizedTask => task !== null);
}
/**
* Load plan.json or fix-plan.json from session directory
* @param sessionPath - Session directory path
@@ -368,14 +506,19 @@ function calculateProgress(tasks: NormalizedTask[]): Progress {
/**
* Get detailed lite task info
* @param workflowDir - Workflow directory
* @param type - 'lite-plan' or 'lite-fix'
* @param type - 'lite-plan', 'lite-fix', or 'multi-cli-plan'
* @param sessionId - Session ID
* @returns Detailed task info
*/
export async function getLiteTaskDetail(workflowDir: string, type: string, sessionId: string): Promise<LiteTaskDetail | null> {
const dir = type === 'lite-plan'
? join(workflowDir, '.lite-plan', sessionId)
: join(workflowDir, '.lite-fix', sessionId);
let dir: string;
if (type === 'lite-plan') {
dir = join(workflowDir, '.lite-plan', sessionId);
} else if (type === 'multi-cli-plan') {
dir = join(workflowDir, '.multi-cli-plan', sessionId);
} else {
dir = join(workflowDir, '.lite-fix', sessionId);
}
try {
const stats = await stat(dir);
@@ -384,6 +527,29 @@ export async function getLiteTaskDetail(workflowDir: string, type: string, sessi
return null;
}
// For multi-cli-plan, use synthesis-based loading
if (type === 'multi-cli-plan') {
const [syntheses, explorations, clarifications] = await Promise.all([
loadRoundSyntheses(dir),
loadExplorations(dir),
loadClarifications(dir),
]);
const latestSynthesis = syntheses.length > 0 ? syntheses[syntheses.length - 1] : null;
const detail: LiteTaskDetail = {
id: sessionId,
type,
path: dir,
plan: latestSynthesis,
tasks: extractTasksFromSyntheses(syntheses),
explorations,
clarifications,
};
return detail;
}
const [plan, tasks, explorations, clarifications, diagnoses] = await Promise.all([
loadPlanJson(dir),
loadTaskJsons(dir),

View File

@@ -7,9 +7,9 @@ import { join } from 'path';
import type { RouteContext } from './types.js';
/**
* Get session detail data (context, summaries, impl-plan, review)
* Get session detail data (context, summaries, impl-plan, review, multi-cli)
* @param {string} sessionPath - Path to session directory
* @param {string} dataType - Type of data to load ('all', 'context', 'tasks', 'summary', 'plan', 'explorations', 'conflict', 'impl-plan', 'review')
* @param {string} dataType - Type of data to load ('all', 'context', 'tasks', 'summary', 'plan', 'explorations', 'conflict', 'impl-plan', 'review', 'multi-cli', 'discussions')
* @returns {Promise<Object>}
*/
async function getSessionDetailData(sessionPath: string, dataType: string): Promise<Record<string, unknown>> {
@@ -251,6 +251,44 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
}
}
// Load multi-cli discussion rounds (rounds/*/synthesis.json)
if (dataType === 'multi-cli' || dataType === 'discussions' || dataType === 'all') {
result.multiCli = {
sessionId: normalizedPath.split('/').pop() || '',
type: 'multi-cli-plan',
rounds: [] as Array<{ roundNumber: number; synthesis: Record<string, unknown> | null }>
};
const roundsDir = join(normalizedPath, 'rounds');
if (existsSync(roundsDir)) {
try {
const roundDirs = readdirSync(roundsDir)
.filter(d => /^\d+$/.test(d)) // Only numeric directories
.sort((a, b) => parseInt(a) - parseInt(b));
for (const roundDir of roundDirs) {
const synthesisFile = join(roundsDir, roundDir, 'synthesis.json');
let synthesis: Record<string, unknown> | null = null;
if (existsSync(synthesisFile)) {
try {
synthesis = JSON.parse(readFileSync(synthesisFile, 'utf8'));
} catch (e) {
// Skip unreadable synthesis files
}
}
result.multiCli.rounds.push({
roundNumber: parseInt(roundDir),
synthesis
});
}
} catch (e) {
// Directory read failed
}
}
}
// Load review data from .review/
if (dataType === 'review' || dataType === 'all') {
const reviewDir = join(normalizedPath, '.review');

View File

@@ -119,6 +119,14 @@ body {
color: hsl(var(--orange));
}
.nav-item[data-lite="multi-cli-plan"].active {
background-color: hsl(var(--purple-light, 280 60% 95%));
}
.nav-item[data-lite="multi-cli-plan"].active .nav-icon {
color: hsl(var(--purple, 280 60% 50%));
}
.sidebar.collapsed .toggle-icon {
transform: rotate(180deg);
}

View File

@@ -1179,3 +1179,799 @@
line-height: 1.5;
}
/* ===================================
Multi-CLI Discussion View Styles
=================================== */
/* Multi-CLI Card (List View) */
.multi-cli-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.multi-cli-card:hover {
border-color: hsl(var(--purple, 280 60% 50%) / 0.5);
box-shadow: 0 4px 12px hsl(var(--purple, 280 60% 50%) / 0.1);
}
.multi-cli-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.multi-cli-card-title {
font-weight: 600;
font-size: 0.95rem;
color: hsl(var(--foreground));
line-height: 1.4;
flex: 1;
}
.multi-cli-card-meta {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
}
.multi-cli-round-count {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: hsl(var(--purple-light, 280 60% 95%));
color: hsl(var(--purple, 280 60% 50%));
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
}
/* Multi-CLI Status Badges */
.multi-cli-status {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
}
.multi-cli-status.converged {
background: hsl(var(--success-light, 142 70% 95%));
color: hsl(var(--success, 142 70% 45%));
}
.multi-cli-status.analyzing {
background: hsl(var(--warning-light, 45 90% 95%));
color: hsl(var(--warning, 45 90% 40%));
}
.multi-cli-status.blocked {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
.multi-cli-status.pending {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
/* Multi-CLI Detail Page */
.multi-cli-detail-page {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.multi-cli-detail-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid hsl(var(--border));
}
.multi-cli-detail-info {
flex: 1;
}
.multi-cli-detail-title {
font-size: 1.25rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
.multi-cli-detail-meta {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.85rem;
color: hsl(var(--muted-foreground));
}
/* Multi-CLI Tabs */
.multi-cli-tabs {
display: flex;
gap: 0.25rem;
border-bottom: 1px solid hsl(var(--border));
margin-bottom: 1rem;
overflow-x: auto;
}
.multi-cli-tab {
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.multi-cli-tab:hover {
color: hsl(var(--foreground));
background: hsl(var(--hover));
}
.multi-cli-tab.active {
color: hsl(var(--purple, 280 60% 50%));
border-bottom-color: hsl(var(--purple, 280 60% 50%));
}
.multi-cli-tab-content {
display: none;
}
.multi-cli-tab-content.active {
display: block;
}
/* Multi-CLI Topic Tab */
.multi-cli-topic-section {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
}
.multi-cli-topic-title {
font-size: 1rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.multi-cli-topic-description {
font-size: 0.875rem;
color: hsl(var(--foreground));
line-height: 1.6;
white-space: pre-wrap;
}
.multi-cli-complexity-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
}
.multi-cli-complexity-badge.high {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
.multi-cli-complexity-badge.medium {
background: hsl(var(--warning-light, 45 90% 95%));
color: hsl(var(--warning, 45 90% 40%));
}
.multi-cli-complexity-badge.low {
background: hsl(var(--success-light, 142 70% 95%));
color: hsl(var(--success, 142 70% 45%));
}
/* Multi-CLI Files Tab */
.multi-cli-files-section {
margin-bottom: 1rem;
}
.multi-cli-files-title {
font-size: 0.9rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.file-tree {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 0.75rem;
}
.file-tree-node {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.5rem;
font-size: 0.85rem;
border-radius: 0.25rem;
transition: background 0.15s;
}
.file-tree-node:hover {
background: hsl(var(--hover));
}
.file-tree-node.directory {
color: hsl(var(--primary));
font-weight: 500;
}
.file-tree-node.file {
color: hsl(var(--foreground));
padding-left: 1.5rem;
}
.file-tree-node .file-icon {
width: 1rem;
height: 1rem;
color: hsl(var(--muted-foreground));
}
.file-tree-node.directory .file-icon {
color: hsl(var(--primary));
}
.file-purpose {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
margin-left: auto;
}
/* Multi-CLI Planning Tab */
.multi-cli-planning-section {
margin-bottom: 1.5rem;
}
.planning-section-title {
font-size: 0.9rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.requirements-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.requirement-item {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 0.75rem;
}
.requirement-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.requirement-id {
font-weight: 600;
font-size: 0.8rem;
color: hsl(var(--primary));
}
.requirement-priority {
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.7rem;
font-weight: 500;
}
.requirement-priority.high {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
.requirement-priority.medium {
background: hsl(var(--warning-light, 45 90% 95%));
color: hsl(var(--warning, 45 90% 40%));
}
.requirement-priority.low {
background: hsl(var(--success-light, 142 70% 95%));
color: hsl(var(--success, 142 70% 45%));
}
.requirement-title {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
margin-bottom: 0.25rem;
}
.requirement-description {
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
line-height: 1.5;
}
.impact-items {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.impact-item {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: hsl(var(--muted));
border-radius: 0.25rem;
font-size: 0.75rem;
color: hsl(var(--foreground));
}
.impact-item .impact-icon {
width: 0.875rem;
height: 0.875rem;
color: hsl(var(--muted-foreground));
}
/* Multi-CLI Decision Tab */
.multi-cli-decision-section {
margin-bottom: 1.5rem;
}
.decision-section-title {
font-size: 0.9rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.solutions-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.solution-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
transition: all 0.2s ease;
}
.solution-card.selected {
border-color: hsl(var(--success, 142 70% 45%));
background: hsl(var(--success, 142 70% 45%) / 0.05);
}
.solution-card.rejected {
border-color: hsl(var(--destructive) / 0.5);
background: hsl(var(--destructive) / 0.03);
opacity: 0.8;
}
.solution-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.solution-title {
font-weight: 600;
font-size: 0.95rem;
color: hsl(var(--foreground));
display: flex;
align-items: center;
gap: 0.5rem;
}
.solution-status {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
}
.solution-status.selected {
background: hsl(var(--success, 142 70% 45%) / 0.15);
color: hsl(var(--success, 142 70% 45%));
}
.solution-status.rejected {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
.solution-status.considering {
background: hsl(var(--warning-light, 45 90% 95%));
color: hsl(var(--warning, 45 90% 40%));
}
.solution-description {
font-size: 0.85rem;
color: hsl(var(--foreground));
line-height: 1.6;
margin-bottom: 0.75rem;
}
.solution-meta {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.solution-meta-item {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
}
.confidence-meter {
display: flex;
align-items: center;
gap: 0.5rem;
}
.confidence-bar {
width: 60px;
height: 6px;
background: hsl(var(--muted));
border-radius: 3px;
overflow: hidden;
}
.confidence-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.confidence-fill.high {
background: hsl(var(--success, 142 70% 45%));
}
.confidence-fill.medium {
background: hsl(var(--warning, 45 90% 50%));
}
.confidence-fill.low {
background: hsl(var(--destructive));
}
.confidence-value {
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--foreground));
}
/* Multi-CLI Timeline Tab */
.multi-cli-timeline {
display: flex;
flex-direction: column;
gap: 0;
}
.timeline-event {
display: flex;
gap: 1rem;
position: relative;
}
.timeline-marker {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
width: 2rem;
}
.timeline-dot {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
flex-shrink: 0;
}
.timeline-dot.proposal {
background: hsl(var(--primary));
color: white;
}
.timeline-dot.analysis {
background: hsl(var(--info, 220 80% 55%));
color: white;
}
.timeline-dot.decision {
background: hsl(var(--success, 142 70% 45%));
color: white;
}
.timeline-dot.conflict {
background: hsl(var(--warning, 45 90% 50%));
color: hsl(var(--foreground));
}
.timeline-dot.resolution {
background: hsl(var(--purple, 280 60% 50%));
color: white;
}
.timeline-dot i {
width: 0.75rem;
height: 0.75rem;
}
.timeline-line {
width: 2px;
flex: 1;
min-height: 1rem;
background: hsl(var(--border));
margin: 0.25rem 0;
}
.timeline-content {
flex: 1;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 0.75rem;
margin-bottom: 0.75rem;
}
.timeline-content:hover {
border-color: hsl(var(--primary) / 0.3);
}
.timeline-event-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.timeline-event-type {
font-weight: 600;
font-size: 0.85rem;
color: hsl(var(--foreground));
}
.timeline-event-time {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
.timeline-event-agent {
font-size: 0.75rem;
color: hsl(var(--purple, 280 60% 50%));
margin-bottom: 0.25rem;
}
.timeline-event-description {
font-size: 0.85rem;
color: hsl(var(--foreground));
line-height: 1.5;
}
/* Multi-CLI Rounds Tab */
.multi-cli-rounds-nav {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid hsl(var(--border));
}
.round-nav-btn {
padding: 0.5rem 1rem;
background: hsl(var(--muted));
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
font-size: 0.85rem;
font-weight: 500;
color: hsl(var(--foreground));
cursor: pointer;
transition: all 0.2s ease;
}
.round-nav-btn:hover {
background: hsl(var(--hover));
border-color: hsl(var(--purple, 280 60% 50%) / 0.5);
}
.round-nav-btn.active {
background: hsl(var(--purple, 280 60% 50%));
border-color: hsl(var(--purple, 280 60% 50%));
color: white;
}
.round-content {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem;
}
.round-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid hsl(var(--border));
}
.round-title {
font-weight: 600;
font-size: 1rem;
color: hsl(var(--foreground));
}
.round-timestamp {
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
}
.round-section {
margin-bottom: 1rem;
}
.round-section:last-child {
margin-bottom: 0;
}
.round-section-title {
font-size: 0.85rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.round-agents {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.round-agent-item {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
padding: 0.75rem;
}
.round-agent-name {
font-weight: 500;
font-size: 0.85rem;
color: hsl(var(--purple, 280 60% 50%));
margin-bottom: 0.25rem;
}
.round-agent-response {
font-size: 0.8rem;
color: hsl(var(--foreground));
line-height: 1.5;
white-space: pre-wrap;
}
.round-convergence {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
}
.convergence-indicator {
font-weight: 500;
font-size: 0.85rem;
}
.convergence-indicator.converged {
color: hsl(var(--success, 142 70% 45%));
}
.convergence-indicator.not-converged {
color: hsl(var(--warning, 45 90% 40%));
}
.round-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
color: hsl(var(--muted-foreground));
font-size: 0.9rem;
}
/* Multi-CLI Empty States */
.multi-cli-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
text-align: center;
color: hsl(var(--muted-foreground));
}
.multi-cli-empty-icon {
width: 3rem;
height: 3rem;
margin-bottom: 1rem;
color: hsl(var(--muted-foreground) / 0.5);
}
.multi-cli-empty-title {
font-size: 1rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: hsl(var(--foreground));
}
.multi-cli-empty-description {
font-size: 0.875rem;
}

View File

@@ -699,6 +699,28 @@
color: hsl(var(--foreground));
}
.file-browser-path:focus {
outline: none;
border-color: hsl(var(--primary));
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.2);
}
.file-browser-drives {
display: flex;
gap: 0.25rem;
}
.btn-xs {
padding: 0.25rem 0.5rem;
font-size: 0.6875rem;
border-radius: 0.25rem;
}
.drive-btn {
font-family: monospace;
font-weight: 600;
}
.file-browser-hidden-toggle {
display: flex;
align-items: center;

View File

@@ -216,8 +216,10 @@ function updateContentTitle() {
} else if (currentView === 'issue-discovery') {
titleEl.textContent = t('title.issueDiscovery');
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions'), 'multi-cli-plan': t('title.multiCliPlanSessions') || 'Multi-CLI Plan Sessions' };
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
} else if (currentView === 'multiCliDetail') {
titleEl.textContent = t('title.multiCliDetail') || 'Multi-CLI Discussion Detail';
} else if (currentView === 'sessionDetail') {
titleEl.textContent = t('title.sessionDetail');
} else if (currentView === 'liteTaskDetail') {
@@ -322,9 +324,11 @@ function updateSidebarCounts(data) {
// Update lite task counts
const litePlanCount = document.querySelector('.nav-item[data-lite="lite-plan"] .nav-count');
const liteFixCount = document.querySelector('.nav-item[data-lite="lite-fix"] .nav-count');
const multiCliPlanCount = document.querySelector('.nav-item[data-lite="multi-cli-plan"] .nav-count');
if (litePlanCount) litePlanCount.textContent = data.liteTasks?.litePlan?.length || 0;
if (liteFixCount) liteFixCount.textContent = data.liteTasks?.liteFix?.length || 0;
if (multiCliPlanCount) multiCliPlanCount.textContent = data.liteTasks?.multiCliPlan?.length || 0;
}
// ========== Navigation Badge Aggregation ==========

View File

@@ -83,7 +83,8 @@ const i18n = {
'nav.liteTasks': 'Lite Tasks',
'nav.litePlan': 'Lite Plan',
'nav.liteFix': 'Lite Fix',
'nav.multiCliPlan': 'Multi-CLI Plan',
// Sidebar - MCP section
'nav.mcpServers': 'MCP Servers',
'nav.manage': 'Manage',
@@ -119,9 +120,11 @@ const i18n = {
'title.cliHistory': 'CLI Execution History',
'title.litePlanSessions': 'Lite Plan Sessions',
'title.liteFixSessions': 'Lite Fix Sessions',
'title.multiCliPlanSessions': 'Multi-CLI Plan Sessions',
'title.liteTasks': 'Lite Tasks',
'title.sessionDetail': 'Session Detail',
'title.liteTaskDetail': 'Lite Task Detail',
'title.multiCliDetail': 'Multi-CLI Discussion Detail',
'title.hookManager': 'Hook Manager',
'title.memoryModule': 'Memory Module',
'title.promptHistory': 'Prompt History',
@@ -268,6 +271,7 @@ const i18n = {
'cli.envFilePlaceholder': 'Path to .env file (e.g., ~/.gemini-env or C:/Users/xxx/.env)',
'cli.envFileHint': 'Load environment variables (e.g., API keys) before CLI execution. Supports ~ for home directory.',
'cli.envFileBrowse': 'Browse',
'cli.envFilePathHint': 'Please verify or complete the file path (e.g., ~/.gemini-env)',
'cli.fileBrowser': 'File Browser',
'cli.fileBrowserSelect': 'Select',
'cli.fileBrowserCancel': 'Cancel',
@@ -1201,7 +1205,62 @@ const i18n = {
'lite.diagnosisDetails': 'Diagnosis Details',
'lite.totalDiagnoses': 'Total Diagnoses:',
'lite.angles': 'Angles:',
'lite.multiCli': 'Multi-CLI',
// Multi-CLI Plan
'multiCli.rounds': 'rounds',
'multiCli.backToList': 'Back to Multi-CLI Plan',
'multiCli.roundCount': 'Rounds',
'multiCli.topic': 'Topic',
'multiCli.tab.topic': 'Discussion Topic',
'multiCli.tab.files': 'Related Files',
'multiCli.tab.planning': 'Planning',
'multiCli.tab.decision': 'Decision',
'multiCli.tab.timeline': 'Timeline',
'multiCli.tab.rounds': 'Rounds',
'multiCli.scope': 'Scope',
'multiCli.scope.included': 'Included',
'multiCli.scope.excluded': 'Excluded',
'multiCli.keyQuestions': 'Key Questions',
'multiCli.fileTree': 'File Tree',
'multiCli.impactSummary': 'Impact Summary',
'multiCli.dependencies': 'Dependencies',
'multiCli.functional': 'Functional Requirements',
'multiCli.nonFunctional': 'Non-Functional Requirements',
'multiCli.acceptanceCriteria': 'Acceptance Criteria',
'multiCli.source': 'Source',
'multiCli.confidence': 'Confidence',
'multiCli.selectedSolution': 'Selected Solution',
'multiCli.rejectedAlternatives': 'Rejected Alternatives',
'multiCli.rejectionReason': 'Reason',
'multiCli.pros': 'Pros',
'multiCli.cons': 'Cons',
'multiCli.effort': 'Effort',
'multiCli.sources': 'Sources',
'multiCli.currentRound': 'Current',
'multiCli.singleRoundInfo': 'This is a single-round discussion. View other tabs for details.',
'multiCli.noRoundData': 'No data for this round.',
'multiCli.roundId': 'Round',
'multiCli.timestamp': 'Time',
'multiCli.duration': 'Duration',
'multiCli.contributors': 'Contributors',
'multiCli.convergence': 'Convergence',
'multiCli.newInsights': 'New Insights',
'multiCli.crossVerification': 'Cross-Verification',
'multiCli.agreements': 'Agreements',
'multiCli.disagreements': 'Disagreements',
'multiCli.resolution': 'Resolution',
'multiCli.empty.topic': 'No Discussion Topic',
'multiCli.empty.topicText': 'No discussion topic data available for this session.',
'multiCli.empty.files': 'No Related Files',
'multiCli.empty.filesText': 'No file analysis data available for this session.',
'multiCli.empty.planning': 'No Planning Data',
'multiCli.empty.planningText': 'No planning requirements available for this session.',
'multiCli.empty.decision': 'No Decision Yet',
'multiCli.empty.decisionText': 'No decision has been made for this discussion yet.',
'multiCli.empty.timeline': 'No Timeline Events',
'multiCli.empty.timelineText': 'No decision timeline available for this session.',
// Modals
'modal.contentPreview': 'Content Preview',
'modal.raw': 'Raw',
@@ -2263,7 +2322,8 @@ const i18n = {
'nav.liteTasks': '轻量任务',
'nav.litePlan': '轻量规划',
'nav.liteFix': '轻量修复',
'nav.multiCliPlan': '多CLI规划',
// Sidebar - MCP section
'nav.mcpServers': 'MCP 服务器',
'nav.manage': '管理',
@@ -2299,9 +2359,11 @@ const i18n = {
'title.cliHistory': 'CLI 执行历史',
'title.litePlanSessions': '轻量规划会话',
'title.liteFixSessions': '轻量修复会话',
'title.multiCliPlanSessions': '多CLI规划会话',
'title.liteTasks': '轻量任务',
'title.sessionDetail': '会话详情',
'title.liteTaskDetail': '轻量任务详情',
'title.multiCliDetail': '多CLI讨论详情',
'title.hookManager': '钩子管理',
'title.memoryModule': '记忆模块',
'title.promptHistory': '提示历史',
@@ -2448,6 +2510,7 @@ const i18n = {
'cli.envFilePlaceholder': '.env 文件路径(如 ~/.gemini-env 或 C:/Users/xxx/.env',
'cli.envFileHint': '在 CLI 执行前加载环境变量(如 API 密钥)。支持 ~ 表示用户目录。',
'cli.envFileBrowse': '浏览',
'cli.envFilePathHint': '请确认或补全文件路径(如 ~/.gemini-env',
'cli.fileBrowser': '文件浏览器',
'cli.fileBrowserSelect': '选择',
'cli.fileBrowserCancel': '取消',
@@ -3360,7 +3423,62 @@ const i18n = {
'lite.diagnosisDetails': '诊断详情',
'lite.totalDiagnoses': '总诊断数:',
'lite.angles': '分析角度:',
'lite.multiCli': '多CLI',
// Multi-CLI Plan
'multiCli.rounds': '轮',
'multiCli.backToList': '返回多CLI计划',
'multiCli.roundCount': '轮数',
'multiCli.topic': '主题',
'multiCli.tab.topic': '讨论主题',
'multiCli.tab.files': '相关文件',
'multiCli.tab.planning': '规划',
'multiCli.tab.decision': '决策',
'multiCli.tab.timeline': '时间线',
'multiCli.tab.rounds': '轮次',
'multiCli.scope': '范围',
'multiCli.scope.included': '包含',
'multiCli.scope.excluded': '排除',
'multiCli.keyQuestions': '关键问题',
'multiCli.fileTree': '文件树',
'multiCli.impactSummary': '影响摘要',
'multiCli.dependencies': '依赖关系',
'multiCli.functional': '功能需求',
'multiCli.nonFunctional': '非功能需求',
'multiCli.acceptanceCriteria': '验收标准',
'multiCli.source': '来源',
'multiCli.confidence': '置信度',
'multiCli.selectedSolution': '选定方案',
'multiCli.rejectedAlternatives': '被拒绝的备选方案',
'multiCli.rejectionReason': '原因',
'multiCli.pros': '优点',
'multiCli.cons': '缺点',
'multiCli.effort': '工作量',
'multiCli.sources': '来源',
'multiCli.currentRound': '当前',
'multiCli.singleRoundInfo': '这是单轮讨论。查看其他标签页获取详情。',
'multiCli.noRoundData': '此轮无数据。',
'multiCli.roundId': '轮次',
'multiCli.timestamp': '时间',
'multiCli.duration': '持续时间',
'multiCli.contributors': '贡献者',
'multiCli.convergence': '收敛度',
'multiCli.newInsights': '新发现',
'multiCli.crossVerification': '交叉验证',
'multiCli.agreements': '一致意见',
'multiCli.disagreements': '分歧',
'multiCli.resolution': '决议',
'multiCli.empty.topic': '无讨论主题',
'multiCli.empty.topicText': '此会话无可用的讨论主题数据。',
'multiCli.empty.files': '无相关文件',
'multiCli.empty.filesText': '此会话无可用的文件分析数据。',
'multiCli.empty.planning': '无规划数据',
'multiCli.empty.planningText': '此会话无可用的规划需求。',
'multiCli.empty.decision': '暂无决策',
'multiCli.empty.decisionText': '此讨论尚未做出决策。',
'multiCli.empty.timeline': '无时间线事件',
'multiCli.empty.timelineText': '此会话无可用的决策时间线。',
// Modals
'modal.contentPreview': '内容预览',
'modal.raw': '原始',

View File

@@ -584,6 +584,17 @@ function showFileBrowserModal(onSelect) {
}
function buildFileBrowserModalContent() {
// Detect if Windows
var isWindows = navigator.platform.indexOf('Win') > -1;
var driveButtons = '';
if (isWindows) {
driveButtons = '<div class="file-browser-drives">' +
'<button class="btn-xs btn-outline drive-btn" data-drive="C:/">C:</button>' +
'<button class="btn-xs btn-outline drive-btn" data-drive="D:/">D:</button>' +
'<button class="btn-xs btn-outline drive-btn" data-drive="E:/">E:</button>' +
'</div>';
}
return '<div class="modal-content file-browser-modal">' +
'<div class="modal-header">' +
'<h3><i data-lucide="folder-open" class="w-4 h-4"></i> ' + t('cli.fileBrowser') + '</h3>' +
@@ -597,9 +608,10 @@ function buildFileBrowserModalContent() {
'<button class="btn-sm btn-outline" id="fileBrowserHomeBtn" title="' + t('cli.fileBrowserHome') + '">' +
'<i data-lucide="home" class="w-3.5 h-3.5"></i>' +
'</button>' +
'<input type="text" id="fileBrowserPathInput" class="file-browser-path" placeholder="/" readonly />' +
driveButtons +
'<input type="text" id="fileBrowserPathInput" class="file-browser-path" placeholder="Enter path and press Enter" />' +
'<label class="file-browser-hidden-toggle">' +
'<input type="checkbox" id="fileBrowserShowHidden" />' +
'<input type="checkbox" id="fileBrowserShowHidden" checked />' +
'<span>' + t('cli.fileBrowserShowHidden') + '</span>' +
'</label>' +
'</div>' +
@@ -756,9 +768,35 @@ function initFileBrowserEvents() {
};
}
// Drive buttons (Windows)
document.querySelectorAll('.drive-btn').forEach(function(btn) {
btn.onclick = function() {
var drive = btn.getAttribute('data-drive');
if (drive) {
loadFileBrowserDirectory(drive);
}
};
});
// Path input - allow manual entry
var pathInput = document.getElementById('fileBrowserPathInput');
if (pathInput) {
pathInput.onkeydown = function(e) {
if (e.key === 'Enter') {
e.preventDefault();
var path = pathInput.value.trim();
if (path) {
loadFileBrowserDirectory(path);
}
}
};
}
// Show hidden checkbox
var showHiddenCheckbox = document.getElementById('fileBrowserShowHidden');
if (showHiddenCheckbox) {
showHiddenCheckbox.checked = true; // Default to show hidden
fileBrowserState.showHidden = true;
showHiddenCheckbox.onchange = function() {
fileBrowserState.showHidden = showHiddenCheckbox.checked;
loadFileBrowserDirectory(fileBrowserState.currentPath);
@@ -994,6 +1032,7 @@ function initToolConfigModalEvents(tool, currentConfig, models) {
var envFileInput = document.getElementById('envFileInput');
if (envFileInput && selectedPath) {
envFileInput.value = selectedPath;
envFileInput.focus();
}
});
};

View File

@@ -7,9 +7,17 @@ function renderLiteTasks() {
const container = document.getElementById('mainContent');
const liteTasks = workflowData.liteTasks || {};
const sessions = currentLiteType === 'lite-plan'
? liteTasks.litePlan || []
: liteTasks.liteFix || [];
let sessions;
if (currentLiteType === 'lite-plan') {
sessions = liteTasks.litePlan || [];
} else if (currentLiteType === 'lite-fix') {
sessions = liteTasks.liteFix || [];
} else if (currentLiteType === 'multi-cli-plan') {
sessions = liteTasks.multiCliPlan || [];
} else {
sessions = [];
}
if (sessions.length === 0) {
container.innerHTML = `
@@ -23,7 +31,12 @@ function renderLiteTasks() {
return;
}
container.innerHTML = `<div class="sessions-grid">${sessions.map(session => renderLiteTaskCard(session)).join('')}</div>`;
// Render based on type
if (currentLiteType === 'multi-cli-plan') {
container.innerHTML = `<div class="sessions-grid">${sessions.map(session => renderMultiCliCard(session)).join('')}</div>`;
} else {
container.innerHTML = `<div class="sessions-grid">${sessions.map(session => renderLiteTaskCard(session)).join('')}</div>`;
}
// Initialize Lucide icons
if (typeof lucide !== 'undefined') lucide.createIcons();
@@ -68,6 +81,881 @@ function renderLiteTaskCard(session) {
`;
}
// ============================================
// MULTI-CLI PLAN VIEW
// ============================================
/**
* Render a card for multi-cli-plan session
* Shows: Session ID, round count, topic title, status, created date
*/
function renderMultiCliCard(session) {
const sessionKey = `multi-cli-${session.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
liteTaskDataStore[sessionKey] = session;
// Extract info from latest synthesis or metadata
const metadata = session.metadata || {};
const latestSynthesis = session.latestSynthesis || session.discussionTopic || {};
const roundCount = metadata.roundId || session.roundCount || 1;
const topicTitle = getI18nText(latestSynthesis.title) || session.topicTitle || 'Discussion Topic';
const status = latestSynthesis.status || session.status || 'analyzing';
const createdAt = metadata.timestamp || session.createdAt || '';
// Status badge color mapping
const statusColors = {
'decided': 'success',
'converged': 'success',
'exploring': 'info',
'analyzing': 'warning',
'debating': 'warning',
'blocked': 'error',
'conflict': 'error'
};
const statusColor = statusColors[status] || 'default';
return `
<div class="session-card multi-cli-card" onclick="showMultiCliDetailPage('${sessionKey}')" style="cursor: pointer;">
<div class="session-header">
<div class="session-title">${escapeHtml(session.id)}</div>
<span class="session-status multi-cli-plan">
<i data-lucide="messages-square" class="w-3 h-3 inline"></i> ${t('lite.multiCli') || 'Multi-CLI'}
</span>
</div>
<div class="session-body">
<div class="multi-cli-topic">
<i data-lucide="message-circle" class="w-4 h-4 inline mr-1"></i>
<span class="topic-title">${escapeHtml(topicTitle)}</span>
</div>
<div class="session-meta">
<span class="session-meta-item"><i data-lucide="calendar" class="w-3.5 h-3.5 inline mr-1"></i>${formatDate(createdAt)}</span>
<span class="session-meta-item"><i data-lucide="repeat" class="w-3.5 h-3.5 inline mr-1"></i>${roundCount} ${t('multiCli.rounds') || 'rounds'}</span>
<span class="session-meta-item status-badge ${statusColor}"><i data-lucide="activity" class="w-3.5 h-3.5 inline mr-1"></i>${escapeHtml(status)}</span>
</div>
</div>
</div>
`;
}
/**
* Get text from i18n label object (supports {en, zh} format)
*/
function getI18nText(label) {
if (!label) return '';
if (typeof label === 'string') return label;
// Return based on current language or default to English
const lang = window.currentLanguage || 'en';
return label[lang] || label.en || label.zh || '';
}
/**
* Show multi-cli detail page with tabs
*/
function showMultiCliDetailPage(sessionKey) {
const session = liteTaskDataStore[sessionKey];
if (!session) return;
currentView = 'multiCliDetail';
currentSessionDetailKey = sessionKey;
hideStatsAndCarousel();
const container = document.getElementById('mainContent');
const metadata = session.metadata || {};
const discussionTopic = session.discussionTopic || {};
const latestSynthesis = session.latestSynthesis || discussionTopic;
const roundCount = metadata.roundId || session.roundCount || 1;
const topicTitle = getI18nText(latestSynthesis.title) || session.topicTitle || 'Discussion Topic';
const status = latestSynthesis.status || session.status || 'analyzing';
container.innerHTML = `
<div class="session-detail-page multi-cli-detail-page">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToLiteTasks()">
<span class="back-icon">&larr;</span>
<span>${t('multiCli.backToList') || 'Back to Multi-CLI Plan'}</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id"><i data-lucide="messages-square" class="w-5 h-5 inline mr-2"></i> ${escapeHtml(session.id)}</h2>
<div class="detail-badges">
<span class="session-type-badge multi-cli-plan">multi-cli-plan</span>
<span class="session-status-badge ${status}">${escapeHtml(status)}</span>
</div>
</div>
</div>
<!-- Session Info Bar -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">${t('detail.created') || 'Created'}</span>
<span class="info-value">${formatDate(metadata.timestamp || session.createdAt)}</span>
</div>
<div class="info-item">
<span class="info-label">${t('multiCli.roundCount') || 'Rounds'}</span>
<span class="info-value">${roundCount}</span>
</div>
<div class="info-item">
<span class="info-label">${t('multiCli.topic') || 'Topic'}</span>
<span class="info-value">${escapeHtml(topicTitle)}</span>
</div>
</div>
<!-- Tab Navigation -->
<div class="detail-tabs">
<button class="detail-tab active" data-tab="topic" onclick="switchMultiCliDetailTab('topic')">
<span class="tab-icon"><i data-lucide="message-circle" class="w-4 h-4"></i></span>
<span class="tab-text">${t('multiCli.tab.topic') || 'Discussion Topic'}</span>
</button>
<button class="detail-tab" data-tab="files" onclick="switchMultiCliDetailTab('files')">
<span class="tab-icon"><i data-lucide="folder-tree" class="w-4 h-4"></i></span>
<span class="tab-text">${t('multiCli.tab.files') || 'Related Files'}</span>
</button>
<button class="detail-tab" data-tab="planning" onclick="switchMultiCliDetailTab('planning')">
<span class="tab-icon"><i data-lucide="list-checks" class="w-4 h-4"></i></span>
<span class="tab-text">${t('multiCli.tab.planning') || 'Planning'}</span>
</button>
<button class="detail-tab" data-tab="decision" onclick="switchMultiCliDetailTab('decision')">
<span class="tab-icon"><i data-lucide="check-circle" class="w-4 h-4"></i></span>
<span class="tab-text">${t('multiCli.tab.decision') || 'Decision'}</span>
</button>
<button class="detail-tab" data-tab="timeline" onclick="switchMultiCliDetailTab('timeline')">
<span class="tab-icon"><i data-lucide="git-commit" class="w-4 h-4"></i></span>
<span class="tab-text">${t('multiCli.tab.timeline') || 'Timeline'}</span>
</button>
<button class="detail-tab" data-tab="rounds" onclick="switchMultiCliDetailTab('rounds')">
<span class="tab-icon"><i data-lucide="layers" class="w-4 h-4"></i></span>
<span class="tab-text">${t('multiCli.tab.rounds') || 'Rounds'}</span>
<span class="tab-count">${roundCount}</span>
</button>
</div>
<!-- Tab Content -->
<div class="detail-tab-content" id="multiCliDetailTabContent">
${renderMultiCliTopicTab(session)}
</div>
</div>
`;
// Initialize icons and collapsible sections
setTimeout(() => {
if (typeof lucide !== 'undefined') lucide.createIcons();
initCollapsibleSections(container);
}, 50);
}
/**
* Switch between multi-cli detail tabs
*/
function switchMultiCliDetailTab(tabName) {
// Update active tab
document.querySelectorAll('.detail-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
const session = liteTaskDataStore[currentSessionDetailKey];
if (!session) return;
const contentArea = document.getElementById('multiCliDetailTabContent');
switch (tabName) {
case 'topic':
contentArea.innerHTML = renderMultiCliTopicTab(session);
break;
case 'files':
contentArea.innerHTML = renderMultiCliFilesTab(session);
break;
case 'planning':
contentArea.innerHTML = renderMultiCliPlanningTab(session);
break;
case 'decision':
contentArea.innerHTML = renderMultiCliDecisionTab(session);
break;
case 'timeline':
contentArea.innerHTML = renderMultiCliTimelineTab(session);
break;
case 'rounds':
contentArea.innerHTML = renderMultiCliRoundsTab(session);
break;
}
// Re-initialize after tab switch
setTimeout(() => {
if (typeof lucide !== 'undefined') lucide.createIcons();
initCollapsibleSections(contentArea);
}, 50);
}
// ============================================
// MULTI-CLI TAB RENDERERS
// ============================================
/**
* Render Discussion Topic tab
* Shows: title, description, scope, keyQuestions, status, tags
*/
function renderMultiCliTopicTab(session) {
const topic = session.discussionTopic || session.latestSynthesis?.discussionTopic || {};
if (!topic || Object.keys(topic).length === 0) {
return `
<div class="tab-empty-state">
<div class="empty-icon"><i data-lucide="message-circle" class="w-12 h-12"></i></div>
<div class="empty-title">${t('multiCli.empty.topic') || 'No Discussion Topic'}</div>
<div class="empty-text">${t('multiCli.empty.topicText') || 'No discussion topic data available for this session.'}</div>
</div>
`;
}
const title = getI18nText(topic.title) || 'Untitled';
const description = getI18nText(topic.description) || '';
const scope = topic.scope || {};
const keyQuestions = topic.keyQuestions || [];
const status = topic.status || 'unknown';
const tags = topic.tags || [];
let sections = [];
// Title and Description
sections.push(`
<div class="multi-cli-section topic-header-section">
<h3 class="topic-main-title">${escapeHtml(title)}</h3>
${description ? `<p class="topic-description">${escapeHtml(description)}</p>` : ''}
<div class="topic-meta">
<span class="status-badge ${status}">${escapeHtml(status)}</span>
${tags.length ? tags.map(tag => `<span class="tag-badge">${escapeHtml(tag)}</span>`).join('') : ''}
</div>
</div>
`);
// Scope (included/excluded)
if (scope.included?.length || scope.excluded?.length) {
sections.push(`
<div class="multi-cli-section scope-section">
<h4 class="section-title"><i data-lucide="target" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.scope') || 'Scope'}</h4>
${scope.included?.length ? `
<div class="scope-included">
<strong>${t('multiCli.scope.included') || 'Included'}:</strong>
<ul class="scope-list">
${scope.included.map(item => `<li>${escapeHtml(getI18nText(item))}</li>`).join('')}
</ul>
</div>
` : ''}
${scope.excluded?.length ? `
<div class="scope-excluded">
<strong>${t('multiCli.scope.excluded') || 'Excluded'}:</strong>
<ul class="scope-list excluded">
${scope.excluded.map(item => `<li>${escapeHtml(getI18nText(item))}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`);
}
// Key Questions
if (keyQuestions.length) {
sections.push(`
<div class="multi-cli-section questions-section">
<h4 class="section-title"><i data-lucide="help-circle" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.keyQuestions') || 'Key Questions'}</h4>
<ol class="key-questions-list">
${keyQuestions.map(q => `<li>${escapeHtml(getI18nText(q))}</li>`).join('')}
</ol>
</div>
`);
}
return `<div class="multi-cli-topic-tab">${sections.join('')}</div>`;
}
/**
* Render Related Files tab
* Shows: fileTree, impactSummary
*/
function renderMultiCliFilesTab(session) {
const relatedFiles = session.relatedFiles || session.latestSynthesis?.relatedFiles || {};
if (!relatedFiles || Object.keys(relatedFiles).length === 0) {
return `
<div class="tab-empty-state">
<div class="empty-icon"><i data-lucide="folder-tree" class="w-12 h-12"></i></div>
<div class="empty-title">${t('multiCli.empty.files') || 'No Related Files'}</div>
<div class="empty-text">${t('multiCli.empty.filesText') || 'No file analysis data available for this session.'}</div>
</div>
`;
}
const fileTree = relatedFiles.fileTree || [];
const impactSummary = relatedFiles.impactSummary || [];
const dependencyGraph = relatedFiles.dependencyGraph || [];
let sections = [];
// File Tree
if (fileTree.length) {
sections.push(`
<div class="multi-cli-section file-tree-section collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">&#9658;</span>
<span class="section-label"><i data-lucide="folder-tree" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.fileTree') || 'File Tree'} (${fileTree.length})</span>
</div>
<div class="collapsible-content collapsed">
<div class="file-tree-list">
${renderFileTreeNodes(fileTree)}
</div>
</div>
</div>
`);
}
// Impact Summary
if (impactSummary.length) {
sections.push(`
<div class="multi-cli-section impact-section collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">&#9658;</span>
<span class="section-label"><i data-lucide="alert-triangle" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.impactSummary') || 'Impact Summary'} (${impactSummary.length})</span>
</div>
<div class="collapsible-content collapsed">
<div class="impact-list">
${impactSummary.map(impact => `
<div class="impact-item impact-${impact.score || 'medium'}">
<div class="impact-header">
<code class="impact-file">${escapeHtml(impact.filePath || '')}</code>
${impact.line ? `<span class="impact-line">:${impact.line}</span>` : ''}
<span class="impact-score ${impact.score || 'medium'}">${escapeHtml(impact.score || 'medium')}</span>
</div>
${impact.reasoning ? `<div class="impact-reason">${escapeHtml(getI18nText(impact.reasoning))}</div>` : ''}
</div>
`).join('')}
</div>
</div>
</div>
`);
}
// Dependency Graph
if (dependencyGraph.length) {
sections.push(`
<div class="multi-cli-section deps-section collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">&#9658;</span>
<span class="section-label"><i data-lucide="git-branch" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.dependencies') || 'Dependencies'} (${dependencyGraph.length})</span>
</div>
<div class="collapsible-content collapsed">
<div class="deps-list">
${dependencyGraph.map(edge => `
<div class="dep-edge">
<code>${escapeHtml(edge.source || '')}</code>
<span class="dep-arrow">&rarr;</span>
<code>${escapeHtml(edge.target || '')}</code>
<span class="dep-relationship">(${escapeHtml(edge.relationship || 'depends')})</span>
</div>
`).join('')}
</div>
</div>
</div>
`);
}
return sections.length ? `<div class="multi-cli-files-tab">${sections.join('')}</div>` : `
<div class="tab-empty-state">
<div class="empty-icon"><i data-lucide="folder-tree" class="w-12 h-12"></i></div>
<div class="empty-title">${t('multiCli.empty.files') || 'No Related Files'}</div>
</div>
`;
}
/**
* Render file tree nodes recursively
*/
function renderFileTreeNodes(nodes, depth = 0) {
return nodes.map(node => {
const indent = depth * 16;
const isDir = node.type === 'directory';
const icon = isDir ? 'folder' : 'file';
const modStatus = node.modificationStatus || 'unchanged';
const impactScore = node.impactScore || '';
let html = `
<div class="file-tree-node" style="margin-left: ${indent}px;">
<i data-lucide="${icon}" class="w-4 h-4 inline mr-1 file-icon ${modStatus}"></i>
<span class="file-path ${modStatus}">${escapeHtml(node.path || '')}</span>
${modStatus !== 'unchanged' ? `<span class="mod-status ${modStatus}">${modStatus}</span>` : ''}
${impactScore ? `<span class="impact-badge ${impactScore}">${impactScore}</span>` : ''}
</div>
`;
if (node.children?.length) {
html += renderFileTreeNodes(node.children, depth + 1);
}
return html;
}).join('');
}
/**
* Render Planning tab
* Shows: functional, nonFunctional requirements, acceptanceCriteria
*/
function renderMultiCliPlanningTab(session) {
const planning = session.planning || session.latestSynthesis?.planning || {};
if (!planning || Object.keys(planning).length === 0) {
return `
<div class="tab-empty-state">
<div class="empty-icon"><i data-lucide="list-checks" class="w-12 h-12"></i></div>
<div class="empty-title">${t('multiCli.empty.planning') || 'No Planning Data'}</div>
<div class="empty-text">${t('multiCli.empty.planningText') || 'No planning requirements available for this session.'}</div>
</div>
`;
}
const functional = planning.functional || [];
const nonFunctional = planning.nonFunctional || [];
const acceptanceCriteria = planning.acceptanceCriteria || [];
let sections = [];
// Functional Requirements
if (functional.length) {
sections.push(`
<div class="multi-cli-section requirements-section">
<h4 class="section-title"><i data-lucide="check-square" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.functional') || 'Functional Requirements'} (${functional.length})</h4>
<div class="requirements-list">
${functional.map(req => renderRequirementItem(req)).join('')}
</div>
</div>
`);
}
// Non-Functional Requirements
if (nonFunctional.length) {
sections.push(`
<div class="multi-cli-section requirements-section">
<h4 class="section-title"><i data-lucide="settings" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.nonFunctional') || 'Non-Functional Requirements'} (${nonFunctional.length})</h4>
<div class="requirements-list">
${nonFunctional.map(req => renderRequirementItem(req)).join('')}
</div>
</div>
`);
}
// Acceptance Criteria
if (acceptanceCriteria.length) {
sections.push(`
<div class="multi-cli-section acceptance-section">
<h4 class="section-title"><i data-lucide="clipboard-check" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.acceptanceCriteria') || 'Acceptance Criteria'} (${acceptanceCriteria.length})</h4>
<div class="acceptance-list">
${acceptanceCriteria.map(ac => `
<div class="acceptance-item ${ac.isMet ? 'met' : 'unmet'}">
<span class="acceptance-check">${ac.isMet ? '&#10003;' : '&#9675;'}</span>
<span class="acceptance-id">${escapeHtml(ac.id || '')}</span>
<span class="acceptance-desc">${escapeHtml(getI18nText(ac.description))}</span>
</div>
`).join('')}
</div>
</div>
`);
}
return sections.length ? `<div class="multi-cli-planning-tab">${sections.join('')}</div>` : `
<div class="tab-empty-state">
<div class="empty-icon"><i data-lucide="list-checks" class="w-12 h-12"></i></div>
<div class="empty-title">${t('multiCli.empty.planning') || 'No Planning Data'}</div>
</div>
`;
}
/**
* Render a single requirement item
*/
function renderRequirementItem(req) {
const priorityColors = {
'critical': 'error',
'high': 'warning',
'medium': 'info',
'low': 'default'
};
const priority = req.priority || 'medium';
const colorClass = priorityColors[priority] || 'default';
return `
<div class="requirement-item">
<div class="requirement-header">
<span class="requirement-id">${escapeHtml(req.id || '')}</span>
<span class="priority-badge ${colorClass}">${escapeHtml(priority)}</span>
</div>
<div class="requirement-desc">${escapeHtml(getI18nText(req.description))}</div>
${req.source ? `<div class="requirement-source">${t('multiCli.source') || 'Source'}: ${escapeHtml(req.source)}</div>` : ''}
</div>
`;
}
/**
* Render Decision tab
* Shows: selectedSolution, rejectedAlternatives, confidenceScore
*/
function renderMultiCliDecisionTab(session) {
const decision = session.decision || session.latestSynthesis?.decision || {};
if (!decision || Object.keys(decision).length === 0) {
return `
<div class="tab-empty-state">
<div class="empty-icon"><i data-lucide="check-circle" class="w-12 h-12"></i></div>
<div class="empty-title">${t('multiCli.empty.decision') || 'No Decision Yet'}</div>
<div class="empty-text">${t('multiCli.empty.decisionText') || 'No decision has been made for this discussion yet.'}</div>
</div>
`;
}
const status = decision.status || 'pending';
const summary = getI18nText(decision.summary) || '';
const selectedSolution = decision.selectedSolution || null;
const rejectedAlternatives = decision.rejectedAlternatives || [];
const confidenceScore = decision.confidenceScore || 0;
let sections = [];
// Decision Status and Summary
sections.push(`
<div class="multi-cli-section decision-header-section">
<div class="decision-status-bar">
<span class="decision-status ${status}">${escapeHtml(status)}</span>
<span class="confidence-meter">
<span class="confidence-label">${t('multiCli.confidence') || 'Confidence'}:</span>
<span class="confidence-bar">
<span class="confidence-fill" style="width: ${(confidenceScore * 100).toFixed(0)}%"></span>
</span>
<span class="confidence-value">${(confidenceScore * 100).toFixed(0)}%</span>
</span>
</div>
${summary ? `<p class="decision-summary">${escapeHtml(summary)}</p>` : ''}
</div>
`);
// Selected Solution
if (selectedSolution) {
sections.push(`
<div class="multi-cli-section selected-solution-section">
<h4 class="section-title"><i data-lucide="check-circle" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.selectedSolution') || 'Selected Solution'}</h4>
${renderSolutionCard(selectedSolution, true)}
</div>
`);
}
// Rejected Alternatives
if (rejectedAlternatives.length) {
sections.push(`
<div class="multi-cli-section rejected-section collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">&#9658;</span>
<span class="section-label"><i data-lucide="x-circle" class="w-4 h-4 inline mr-1"></i> ${t('multiCli.rejectedAlternatives') || 'Rejected Alternatives'} (${rejectedAlternatives.length})</span>
</div>
<div class="collapsible-content collapsed">
${rejectedAlternatives.map(alt => renderSolutionCard(alt, false)).join('')}
</div>
</div>
`);
}
return `<div class="multi-cli-decision-tab">${sections.join('')}</div>`;
}
/**
* Render a solution card
*/
function renderSolutionCard(solution, isSelected) {
const title = getI18nText(solution.title) || 'Untitled Solution';
const description = getI18nText(solution.description) || '';
const pros = solution.pros || [];
const cons = solution.cons || [];
const risk = solution.risk || 'medium';
const effort = getI18nText(solution.estimatedEffort) || '';
const rejectionReason = solution.rejectionReason ? getI18nText(solution.rejectionReason) : '';
const sourceCLIs = solution.sourceCLIs || [];
return `
<div class="solution-card ${isSelected ? 'selected' : 'rejected'}">
<div class="solution-header">
<span class="solution-id">${escapeHtml(solution.id || '')}</span>
<span class="solution-title">${escapeHtml(title)}</span>
<span class="risk-badge ${risk}">${escapeHtml(risk)}</span>
</div>
${description ? `<p class="solution-desc">${escapeHtml(description)}</p>` : ''}
${rejectionReason ? `<div class="rejection-reason"><strong>${t('multiCli.rejectionReason') || 'Reason'}:</strong> ${escapeHtml(rejectionReason)}</div>` : ''}
<div class="solution-details">
${pros.length ? `
<div class="pros-list">
<strong>${t('multiCli.pros') || 'Pros'}:</strong>
<ul>${pros.map(p => `<li class="pro-item">${escapeHtml(getI18nText(p))}</li>`).join('')}</ul>
</div>
` : ''}
${cons.length ? `
<div class="cons-list">
<strong>${t('multiCli.cons') || 'Cons'}:</strong>
<ul>${cons.map(c => `<li class="con-item">${escapeHtml(getI18nText(c))}</li>`).join('')}</ul>
</div>
` : ''}
${effort ? `<div class="effort-estimate"><strong>${t('multiCli.effort') || 'Effort'}:</strong> ${escapeHtml(effort)}</div>` : ''}
${sourceCLIs.length ? `<div class="source-clis"><strong>${t('multiCli.sources') || 'Sources'}:</strong> ${sourceCLIs.map(s => `<span class="cli-badge">${escapeHtml(s)}</span>`).join('')}</div>` : ''}
</div>
</div>
`;
}
/**
* Render Timeline tab
* Shows: decisionRecords.timeline
*/
function renderMultiCliTimelineTab(session) {
const decisionRecords = session.decisionRecords || session.latestSynthesis?.decisionRecords || {};
const timeline = decisionRecords.timeline || [];
if (!timeline.length) {
return `
<div class="tab-empty-state">
<div class="empty-icon"><i data-lucide="git-commit" class="w-12 h-12"></i></div>
<div class="empty-title">${t('multiCli.empty.timeline') || 'No Timeline Events'}</div>
<div class="empty-text">${t('multiCli.empty.timelineText') || 'No decision timeline available for this session.'}</div>
</div>
`;
}
const eventTypeIcons = {
'proposal': 'lightbulb',
'argument': 'message-square',
'agreement': 'thumbs-up',
'disagreement': 'thumbs-down',
'decision': 'check-circle',
'reversal': 'rotate-ccw'
};
return `
<div class="multi-cli-timeline-tab">
<div class="timeline-container">
${timeline.map(event => {
const icon = eventTypeIcons[event.type] || 'circle';
const contributor = event.contributor || {};
const summary = getI18nText(event.summary) || '';
const evidence = event.evidence || [];
return `
<div class="timeline-event event-${event.type || 'default'}">
<div class="timeline-marker">
<i data-lucide="${icon}" class="w-4 h-4"></i>
</div>
<div class="timeline-content">
<div class="event-header">
<span class="event-type ${event.type || ''}">${escapeHtml(event.type || 'event')}</span>
<span class="event-contributor">${escapeHtml(contributor.name || 'Unknown')}</span>
<span class="event-time">${formatDate(event.timestamp)}</span>
</div>
<div class="event-summary">${escapeHtml(summary)}</div>
${event.reversibility ? `<span class="reversibility-badge ${event.reversibility}">${escapeHtml(event.reversibility)}</span>` : ''}
${evidence.length ? `
<div class="event-evidence">
${evidence.map(ev => `
<div class="evidence-item evidence-${ev.type || 'reference'}">
<span class="evidence-type">${escapeHtml(ev.type || 'reference')}</span>
<span class="evidence-desc">${escapeHtml(getI18nText(ev.description))}</span>
</div>
`).join('')}
</div>
` : ''}
</div>
</div>
`;
}).join('')}
</div>
</div>
`;
}
/**
* Render Rounds tab
* Shows: navigation between round synthesis files
*/
function renderMultiCliRoundsTab(session) {
const rounds = session.rounds || [];
const metadata = session.metadata || {};
const totalRounds = metadata.roundId || rounds.length || 1;
if (!rounds.length && totalRounds <= 1) {
// Show current synthesis as single round
return `
<div class="multi-cli-rounds-tab">
<div class="rounds-nav">
<div class="round-item active" data-round="1">
<span class="round-number">Round 1</span>
<span class="round-status">${t('multiCli.currentRound') || 'Current'}</span>
</div>
</div>
<div class="round-content">
<div class="round-info">
<p>${t('multiCli.singleRoundInfo') || 'This is a single-round discussion. View other tabs for details.'}</p>
</div>
</div>
</div>
`;
}
// Render round navigation and content
return `
<div class="multi-cli-rounds-tab">
<div class="rounds-nav">
${rounds.map((round, idx) => {
const roundNum = idx + 1;
const isActive = roundNum === totalRounds;
const roundStatus = round.convergence?.recommendation || 'continue';
return `
<div class="round-item ${isActive ? 'active' : ''}" data-round="${roundNum}" onclick="loadMultiCliRound('${currentSessionDetailKey}', ${roundNum})">
<span class="round-number">Round ${roundNum}</span>
<span class="round-status ${roundStatus}">${escapeHtml(roundStatus)}</span>
</div>
`;
}).join('')}
</div>
<div class="round-content" id="multiCliRoundContent">
${renderRoundContent(rounds[totalRounds - 1] || rounds[0] || session)}
</div>
</div>
`;
}
/**
* Render content for a specific round
*/
function renderRoundContent(round) {
if (!round) {
return `<div class="round-empty">${t('multiCli.noRoundData') || 'No data for this round.'}</div>`;
}
const metadata = round.metadata || {};
const agents = metadata.contributingAgents || [];
const convergence = round._internal?.convergence || {};
const crossVerification = round._internal?.cross_verification || {};
let sections = [];
// Round metadata
sections.push(`
<div class="round-metadata">
<div class="meta-row">
<span class="meta-label">${t('multiCli.roundId') || 'Round'}:</span>
<span class="meta-value">${metadata.roundId || 1}</span>
</div>
<div class="meta-row">
<span class="meta-label">${t('multiCli.timestamp') || 'Time'}:</span>
<span class="meta-value">${formatDate(metadata.timestamp)}</span>
</div>
${metadata.durationSeconds ? `
<div class="meta-row">
<span class="meta-label">${t('multiCli.duration') || 'Duration'}:</span>
<span class="meta-value">${metadata.durationSeconds}s</span>
</div>
` : ''}
</div>
`);
// Contributing agents
if (agents.length) {
sections.push(`
<div class="round-agents">
<strong>${t('multiCli.contributors') || 'Contributors'}:</strong>
${agents.map(agent => `<span class="agent-badge">${escapeHtml(agent.name || agent.id)}</span>`).join('')}
</div>
`);
}
// Convergence metrics
if (convergence.score !== undefined) {
sections.push(`
<div class="round-convergence">
<strong>${t('multiCli.convergence') || 'Convergence'}:</strong>
<span class="convergence-score">${(convergence.score * 100).toFixed(0)}%</span>
<span class="convergence-rec ${convergence.recommendation || ''}">${escapeHtml(convergence.recommendation || '')}</span>
${convergence.new_insights ? `<span class="new-insights-badge">${t('multiCli.newInsights') || 'New Insights'}</span>` : ''}
</div>
`);
}
// Cross-verification
if (crossVerification.agreements?.length || crossVerification.disagreements?.length) {
sections.push(`
<div class="round-verification collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">&#9658;</span>
<span class="section-label">${t('multiCli.crossVerification') || 'Cross-Verification'}</span>
</div>
<div class="collapsible-content collapsed">
${crossVerification.agreements?.length ? `
<div class="agreements">
<strong>${t('multiCli.agreements') || 'Agreements'}:</strong>
<ul>${crossVerification.agreements.map(a => `<li class="agreement">${escapeHtml(a)}</li>`).join('')}</ul>
</div>
` : ''}
${crossVerification.disagreements?.length ? `
<div class="disagreements">
<strong>${t('multiCli.disagreements') || 'Disagreements'}:</strong>
<ul>${crossVerification.disagreements.map(d => `<li class="disagreement">${escapeHtml(d)}</li>`).join('')}</ul>
</div>
` : ''}
${crossVerification.resolution ? `
<div class="resolution">
<strong>${t('multiCli.resolution') || 'Resolution'}:</strong>
<p>${escapeHtml(crossVerification.resolution)}</p>
</div>
` : ''}
</div>
</div>
`);
}
return sections.join('');
}
/**
* Load a specific round's data (async, may fetch from server)
*/
async function loadMultiCliRound(sessionKey, roundNum) {
const session = liteTaskDataStore[sessionKey];
if (!session) return;
// Update active state in nav
document.querySelectorAll('.round-item').forEach(item => {
item.classList.toggle('active', parseInt(item.dataset.round) === roundNum);
});
const contentArea = document.getElementById('multiCliRoundContent');
// If we have rounds array, use it
if (session.rounds && session.rounds[roundNum - 1]) {
contentArea.innerHTML = renderRoundContent(session.rounds[roundNum - 1]);
initCollapsibleSections(contentArea);
return;
}
// Otherwise try to fetch from server
if (window.SERVER_MODE && session.path) {
contentArea.innerHTML = `<div class="tab-loading">${t('common.loading') || 'Loading...'}</div>`;
try {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=round&round=${roundNum}`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderRoundContent(data.round || {});
initCollapsibleSections(contentArea);
return;
}
} catch (err) {
console.error('Failed to load round:', err);
}
}
// Fallback
contentArea.innerHTML = `<div class="round-empty">${t('multiCli.noRoundData') || 'No data for this round.'}</div>`;
}
// Lite Task Detail Page
function showLiteTaskDetailPage(sessionKey) {
const session = liteTaskDataStore[sessionKey];

View File

@@ -405,6 +405,11 @@
<span class="nav-text flex-1" data-i18n="nav.liteFix">Lite Fix</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-light text-orange" id="badgeLiteFix">0</span>
</li>
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-lite="multi-cli-plan" data-tooltip="Multi-CLI Plan Sessions">
<i data-lucide="messages-square" class="nav-icon text-purple"></i>
<span class="nav-text flex-1" data-i18n="nav.multiCliPlan">Multi-CLI Plan</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-purple-light text-purple" id="badgeMultiCliPlan">0</span>
</li>
</ul>
</div>