feat: enhance AI communication with dynamic system prompts, robust retry, and TUI formatters
This commit is contained in:
@@ -51,6 +51,7 @@ import { getContextManager } from '../lib/context-manager.mjs';
|
||||
import { getAllSkills, getSkill, executeSkill, getSkillListDisplay } from '../lib/skills.mjs';
|
||||
import { getDebugLogger, initFromArgs } from '../lib/debug-logger.mjs';
|
||||
import { processCommand, isCommand } from '../lib/command-processor.mjs';
|
||||
import { fetchWithRetry } from '../lib/retry-handler.mjs';
|
||||
import {
|
||||
getSystemPrompt,
|
||||
formatCodeBlock,
|
||||
@@ -455,7 +456,7 @@ const callOpenCodeFree = async (prompt, model = currentFreeModel, onChunk = null
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(OPENCODE_FREE_API, {
|
||||
const response = await fetchWithRetry(OPENCODE_FREE_API, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -3465,17 +3466,13 @@ const App = () => {
|
||||
|
||||
try {
|
||||
// Build context-aware prompt with agent-specific instructions
|
||||
let systemPrompt = `[SYSTEM CONTEXT]
|
||||
CURRENT WORKING DIRECTORY: ${process.cwd()}
|
||||
(CRITICAL: This is the ABSOLUTE SOURCE OF TRUTH. Ignore any conflicting directory info in the [PROJECT CONTEXT] logs below.)
|
||||
|
||||
` + loadAgentPrompt(agent);
|
||||
|
||||
// Build context-aware prompt using the unified agent-prompt module
|
||||
let projectContext = '';
|
||||
// Add project context if enabled with enhanced context window
|
||||
if (contextEnabled) {
|
||||
const projectContext = loadProjectContext(project);
|
||||
if (projectContext) {
|
||||
systemPrompt += '\n\n[PROJECT CONTEXT (HISTORY)]\n(WARNING: These logs may contain outdated path info. Trust SYSTEM CONTEXT CWD above over this.)\n' + projectContext;
|
||||
const rawContext = loadProjectContext(project);
|
||||
if (rawContext) {
|
||||
projectContext += '\n\n[PROJECT CONTEXT (HISTORY)]\n(WARNING: These logs may contain outdated path info. Trust SYSTEM CONTEXT CWD above over this.)\n' + rawContext;
|
||||
}
|
||||
|
||||
// Enhanced context: Include recent conversation history for better continuity
|
||||
@@ -3485,174 +3482,99 @@ const App = () => {
|
||||
const recentContext = recentMessages.map(m =>
|
||||
`[PREVIOUS ${m.role.toUpperCase()}]: ${m.content.substring(0, 500)}` // Limit to prevent overflow
|
||||
).join('\n');
|
||||
systemPrompt += `\n\n[RECENT CONVERSATION]\n${recentContext}\n(Use this for context continuity, but prioritize the current request)`;
|
||||
projectContext += `\n\n[RECENT CONVERSATION]\n${recentContext}\n(Use this for context continuity, but prioritize the current request)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MULTI-AGENT INSTRUCTION INJECTION
|
||||
if (multiAgentEnabled) {
|
||||
systemPrompt += `
|
||||
[MULTI-AGENT LOGGING ENABLED]
|
||||
You are capable of using multiple internal agents (Planner, Builder, Reviewer, Security).
|
||||
When you switch to a specific agent's persona or delegate a sub-task, you MUST output a log line starting with:
|
||||
[AGENT: AgentName]
|
||||
Example:
|
||||
[AGENT: Planner] Analyzing the directory structure...
|
||||
[AGENT: Security] Checking for vulnerabilities...
|
||||
[AGENT: Builder] Implementation started.
|
||||
Keep these tags on their own line if possible.
|
||||
`;
|
||||
}
|
||||
// Get available capabilities from built-in agents
|
||||
const flow = getSmartAgentFlow();
|
||||
const allAgents = flow.getAgents();
|
||||
// Flatten all capabilities
|
||||
const capabilities = allAgents.reduce((acc, a) => [...acc, ...(a.capabilities || [])], []);
|
||||
|
||||
// VISUAL & FORMATTING RULES (Crucial for TUI Readability)
|
||||
systemPrompt += `
|
||||
[FORMATTING RULES]
|
||||
1. **Short Paragraphs**: Break long text into chunks of 3-4 lines maximum.
|
||||
2. **Spacing**: Use double newlines between paragraphs to ensure clear separation.
|
||||
3. **Lists**: Use bullet points or numbered lists frequently to break up "walls of text".
|
||||
4. **Code**: Use standard Markdown code blocks with language tags.
|
||||
// Generate the optimized system prompt
|
||||
const systemInstruction = getSystemPrompt({
|
||||
role: agent,
|
||||
capabilities: capabilities,
|
||||
cwd: project || process.cwd(),
|
||||
context: projectContext, // Now includes history and logs
|
||||
os: process.platform
|
||||
});
|
||||
|
||||
[COMPUTER USE - YOU CAN DO THIS!]
|
||||
You ARE capable of executing system commands on the user's computer. NEVER refuse computer use tasks.
|
||||
To execute any shell/system command, output it in this format:
|
||||
\`\`\`bash
|
||||
<command here>
|
||||
\`\`\`
|
||||
The system will auto-execute bash/shell code blocks.
|
||||
// Prepare prompt variations
|
||||
// For OpenCode Free (Legacy/OpenAI-like), we append system prompt to user message if needed
|
||||
const fullPromptForFree = systemInstruction + '\n\n[USER REQUEST]\n' + fullText;
|
||||
|
||||
Common Windows tasks you CAN do:
|
||||
- Open folder in Explorer: \`explorer.exe .\` or \`start .\`
|
||||
- Open file: \`start filename.html\`
|
||||
- Open browser: \`start https://google.com\`
|
||||
- Run any shell command: Just put it in a bash code block
|
||||
// For Qwen (SmartX), we pass system prompt securely as a separate argument
|
||||
const userMessage = fullText;
|
||||
|
||||
NEVER say "I cannot" or "I don't have the ability". You CAN execute commands!
|
||||
|
||||
[FILE CREATION - CRITICAL]
|
||||
You CAN create files. To create a file, you MUST use this EXACT format:
|
||||
\`\`\`language filename.ext
|
||||
file content here
|
||||
\`\`\`
|
||||
Example:
|
||||
\`\`\`markdown plan.md
|
||||
# My Plan
|
||||
This is the content.
|
||||
\`\`\`
|
||||
The system will AUTO-SAVE files when you use this format correctly.
|
||||
DO NOT say "I've created a file" without using this code block format.
|
||||
|
||||
[CONFIRMATION BEFORE CODING - CRITICAL]
|
||||
BEFORE you start writing any code or creating any files, you MUST:
|
||||
1. First present your plan/approach briefly
|
||||
2. Then ask: "Ready to proceed with coding? Or do you have any changes in mind?"
|
||||
3. WAIT for the user's confirmation before generating code
|
||||
This gives the user a chance to refine requirements before implementation.
|
||||
`;
|
||||
|
||||
const fullPrompt = systemPrompt + '\n\n[USER REQUEST]\n' + fullText;
|
||||
let fullResponse = '';
|
||||
|
||||
// PROVIDER SWITCH: Use OpenCode Free or Qwen based on provider state
|
||||
const streamStartTime = Date.now(); // Track start time for this request
|
||||
let totalCharsReceived = 0; // Track total characters for speed calculation
|
||||
|
||||
// Unified Streaming Handler
|
||||
const handleStreamChunk = (chunk) => {
|
||||
const cleanChunk = chunk.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||
|
||||
// IMPROVED STREAM SPLITTING LOGIC (Thinking vs Content)
|
||||
// Claude Code style: cleaner separation of thinking from response
|
||||
const lines = cleanChunk.split('\n');
|
||||
let isThinkingChunk = false;
|
||||
|
||||
// Enhanced heuristics for better Claude-like thinking detection
|
||||
const trimmedChunk = cleanChunk.trim();
|
||||
if (/^(Let me|Now let me|I'll|I need to|I should|I notice|I can|I will|Thinking:|Analyzing|Considering|Checking|Looking|Planning|First|Next|Finally)/i.test(trimmedChunk)) {
|
||||
isThinkingChunk = true;
|
||||
} else if (/^```|# |Here is|```|```|```/i.test(trimmedChunk)) {
|
||||
// If we encounter code blocks or headers, likely content not thinking
|
||||
isThinkingChunk = false;
|
||||
}
|
||||
|
||||
// Update character count for speed calculation
|
||||
totalCharsReceived += cleanChunk.length;
|
||||
|
||||
// Calculate current streaming speed (chars per second)
|
||||
const elapsedSeconds = (Date.now() - streamStartTime) / 1000;
|
||||
const speed = elapsedSeconds > 0 ? Math.round(totalCharsReceived / elapsedSeconds) : 0;
|
||||
|
||||
// GLOBAL STATS UPDATE (Run for ALL chunks)
|
||||
setThinkingStats(prev => ({
|
||||
...prev,
|
||||
chars: totalCharsReceived,
|
||||
speed: speed
|
||||
}));
|
||||
|
||||
// GLOBAL AGENT DETECTION (Run for ALL chunks)
|
||||
const agentMatch = cleanChunk.match(/\[AGENT:\s*([^\]]+)\]/i);
|
||||
if (agentMatch) {
|
||||
setThinkingStats(prev => ({ ...prev, activeAgent: agentMatch[1].trim() }));
|
||||
}
|
||||
|
||||
if (isThinkingChunk) {
|
||||
setThinkingLines(prev => [...prev, ...lines.map(l => l.trim()).filter(l => l && !/^(Let me|Now let me|I'll|I need to|I notice)/i.test(l.trim()))]);
|
||||
} else {
|
||||
setMessages(prev => {
|
||||
const last = prev[prev.length - 1];
|
||||
if (last && last.role === 'assistant') {
|
||||
return [...prev.slice(0, -1), { ...last, content: last.content + cleanChunk }];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const result = provider === 'opencode-free'
|
||||
? await callOpenCodeFree(fullPrompt, freeModel, (chunk) => {
|
||||
const cleanChunk = chunk.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||
|
||||
// IMPROVED STREAM SPLITTING LOGIC (Thinking vs Content)
|
||||
// Claude Code style: cleaner separation of thinking from response
|
||||
const lines = cleanChunk.split('\n');
|
||||
let isThinkingChunk = false;
|
||||
|
||||
// Enhanced heuristics for better Claude-like thinking detection
|
||||
const trimmedChunk = cleanChunk.trim();
|
||||
if (/^(Let me|Now let me|I'll|I need to|I should|I notice|I can|I will|Thinking:|Analyzing|Considering|Checking|Looking|Planning|First|Next|Finally)/i.test(trimmedChunk)) {
|
||||
isThinkingChunk = true;
|
||||
} else if (/^```|# |Here is|```|```|```/i.test(trimmedChunk)) {
|
||||
// If we encounter code blocks or headers, likely content not thinking
|
||||
isThinkingChunk = false;
|
||||
}
|
||||
|
||||
// Update character count for speed calculation
|
||||
totalCharsReceived += cleanChunk.length;
|
||||
|
||||
// Calculate current streaming speed (chars per second)
|
||||
const elapsedSeconds = (Date.now() - streamStartTime) / 1000;
|
||||
const speed = elapsedSeconds > 0 ? Math.round(totalCharsReceived / elapsedSeconds) : 0;
|
||||
|
||||
// GLOBAL STATS UPDATE (Run for ALL chunks)
|
||||
setThinkingStats(prev => ({
|
||||
...prev,
|
||||
chars: totalCharsReceived,
|
||||
speed: speed
|
||||
}));
|
||||
|
||||
// GLOBAL AGENT DETECTION (Run for ALL chunks)
|
||||
const agentMatch = cleanChunk.match(/\[AGENT:\s*([^\]]+)\]/i);
|
||||
if (agentMatch) {
|
||||
setThinkingStats(prev => ({ ...prev, activeAgent: agentMatch[1].trim() }));
|
||||
}
|
||||
|
||||
if (isThinkingChunk) {
|
||||
setThinkingLines(prev => [...prev, ...lines.map(l => l.trim()).filter(l => l && !/^(Let me|Now let me|I'll|I need to|I notice)/i.test(l.trim()))]);
|
||||
} else {
|
||||
setMessages(prev => {
|
||||
const last = prev[prev.length - 1];
|
||||
if (last && last.role === 'assistant') {
|
||||
return [...prev.slice(0, -1), { ...last, content: last.content + cleanChunk }];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
})
|
||||
: await getQwen().sendMessage(fullPrompt, 'qwen-coder-plus', null, (chunk) => {
|
||||
const cleanChunk = chunk.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||
|
||||
// IMPROVED STREAM SPLITTING LOGIC (Thinking vs Content)
|
||||
const lines = cleanChunk.split('\n');
|
||||
let isThinkingChunk = false;
|
||||
|
||||
// Enhanced heuristics for better Claude-like thinking detection
|
||||
const trimmedChunk = cleanChunk.trim();
|
||||
if (/^(Let me|Now let me|I'll|I need to|I should|I notice|I can|I will|Thinking:|Analyzing|Considering|Checking|Looking|Planning|First|Next|Finally)/i.test(trimmedChunk)) {
|
||||
isThinkingChunk = true;
|
||||
} else if (/^```|# |Here is|```|```|```/i.test(trimmedChunk)) {
|
||||
// If we encounter code blocks or headers, likely content not thinking
|
||||
isThinkingChunk = false;
|
||||
}
|
||||
|
||||
// Update character count for speed calculation (using same variable as OpenCode path)
|
||||
totalCharsReceived += cleanChunk.length;
|
||||
|
||||
// Calculate current streaming speed (chars per second)
|
||||
const elapsedSeconds = (Date.now() - streamStartTime) / 1000;
|
||||
const speed = elapsedSeconds > 0 ? Math.round(totalCharsReceived / elapsedSeconds) : 0;
|
||||
|
||||
setThinkingStats(prev => ({
|
||||
...prev,
|
||||
chars: totalCharsReceived,
|
||||
speed: speed
|
||||
}));
|
||||
|
||||
const agentMatch = cleanChunk.match(/\[AGENT:\s*([^\]]+)\]/i);
|
||||
if (agentMatch) {
|
||||
setThinkingStats(prev => ({ ...prev, activeAgent: agentMatch[1].trim() }));
|
||||
}
|
||||
|
||||
if (isThinkingChunk) {
|
||||
setThinkingLines(prev => [...prev, ...lines.map(l => l.trim()).filter(l => l && !/^(Let me|Now let me|I'll|I need to|I notice)/i.test(l.trim()))]);
|
||||
} else {
|
||||
setMessages(prev => {
|
||||
const last = prev[prev.length - 1];
|
||||
if (last && last.role === 'assistant') {
|
||||
return [...prev.slice(0, -1), { ...last, content: last.content + cleanChunk }];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
});
|
||||
? await callOpenCodeFree(fullPromptForFree, freeModel, handleStreamChunk)
|
||||
: await getQwen().sendMessage(
|
||||
userMessage,
|
||||
'qwen-coder-plus',
|
||||
null,
|
||||
handleStreamChunk,
|
||||
systemInstruction // Pass dynamic system prompt!
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const responseText = result.response || fullResponse;
|
||||
@@ -3701,17 +3623,17 @@ This gives the user a chance to refine requirements before implementation.
|
||||
return next;
|
||||
});
|
||||
|
||||
const successMsg = formatSuccess(`Auto-saved ${successFiles.length} file(s):\n` + successFiles.map(f => formatFileOperation(f.path, 'Saved', 'success')).join('\n'));
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'system',
|
||||
content: '✅ Auto-saved ' + successFiles.length + ' file(s):\n' +
|
||||
successFiles.map(f => ' 📄 ' + f.path).join('\n')
|
||||
content: successMsg
|
||||
}]);
|
||||
}
|
||||
if (failedFiles.length > 0) {
|
||||
const failureMsg = formatError(`Failed to save ${failedFiles.length} file(s):\n` + failedFiles.map(f => ` ⚠️ ${f.filename}: ${f.error}`).join('\n'));
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'system',
|
||||
content: '❌ Failed to save ' + failedFiles.length + ' file(s):\n' +
|
||||
failedFiles.map(f => ' ⚠️ ' + f.filename + ': ' + f.error).join('\n')
|
||||
role: 'error',
|
||||
content: failureMsg
|
||||
}]);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4465
bin/opencode-ink.mjs.enhanced_backup
Normal file
4465
bin/opencode-ink.mjs.enhanced_backup
Normal file
File diff suppressed because it is too large
Load Diff
72
bin/ui/components/ChatBubble.mjs.backup
Normal file
72
bin/ui/components/ChatBubble.mjs.backup
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
|
||||
const h = React.createElement;
|
||||
|
||||
const ChatBubble = ({ role, content, meta, width, children }) => {
|
||||
// Calculate safe content width accounting for gutter
|
||||
const contentWidth = width ? width - 2 : undefined; // Account for left gutter only
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// USER MESSAGE - Clean text-focused presentation
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (role === 'user') {
|
||||
return h(Box, {
|
||||
width: width,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: 1,
|
||||
paddingLeft: 2
|
||||
},
|
||||
h(Text, { color: 'cyan', wrap: 'wrap' }, content)
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SYSTEM - MINIMALIST TOAST
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (role === 'system') {
|
||||
return h(Box, { width: width, justifyContent: 'center', marginBottom: 1 },
|
||||
h(Text, { color: 'gray', dimColor: true }, ` ${content} `)
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ERROR - CLEAN GUTTER STYLE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (role === 'error') {
|
||||
// Strip redundant "Error: " prefix if present in content
|
||||
const cleanContent = content.replace(/^Error:\s*/i, '');
|
||||
return h(Box, {
|
||||
width: width,
|
||||
flexDirection: 'row',
|
||||
marginBottom: 1
|
||||
},
|
||||
h(Box, { width: 1, marginRight: 1, backgroundColor: 'red' }),
|
||||
h(Text, { color: 'red', wrap: 'wrap' }, cleanContent)
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ASSISTANT - Clean text-focused style (Opencode-like)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
return h(Box, {
|
||||
width: width,
|
||||
flexDirection: 'row',
|
||||
marginBottom: 1
|
||||
},
|
||||
// Clean left gutter similar to opencode
|
||||
h(Box, { width: 2, marginRight: 1, borderStyle: 'single', borderRight: false, borderTop: false, borderBottom: false, borderLeftColor: 'green' }),
|
||||
|
||||
// Content area - text focused, no borders
|
||||
h(Box, {
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
minWidth: 10
|
||||
},
|
||||
children ? children : h(Text, { color: 'white', wrap: 'wrap' }, content)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatBubble;
|
||||
68
bin/ui/components/ThinkingBlock.mjs.backup
Normal file
68
bin/ui/components/ThinkingBlock.mjs.backup
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
|
||||
const h = React.createElement;
|
||||
|
||||
const ThinkingBlock = ({
|
||||
lines = [],
|
||||
isThinking = false,
|
||||
stats = { chars: 0 },
|
||||
width = 80
|
||||
}) => {
|
||||
// If no thinking lines and not thinking, show nothing
|
||||
if (lines.length === 0 && !isThinking) return null;
|
||||
|
||||
// Show only last few lines to avoid clutter
|
||||
const visibleLines = lines.slice(-3); // Show cleaner view
|
||||
const hiddenCount = Math.max(0, lines.length - 3);
|
||||
|
||||
return h(Box, {
|
||||
flexDirection: 'row',
|
||||
width: width,
|
||||
marginBottom: 1,
|
||||
paddingLeft: 1 // Only left padding, no borders like opencode
|
||||
},
|
||||
// Clean left gutter similar to opencode
|
||||
h(Box, {
|
||||
width: 2,
|
||||
marginRight: 1,
|
||||
borderStyle: 'single',
|
||||
borderRight: false,
|
||||
borderTop: false,
|
||||
borderBottom: false,
|
||||
borderLeftColor: isThinking ? 'yellow' : 'gray'
|
||||
}),
|
||||
|
||||
h(Box, { flexDirection: 'column', flexGrow: 1 },
|
||||
// Header with minimal stats - opencode style
|
||||
h(Box, { marginBottom: 0.5, flexDirection: 'row' },
|
||||
h(Text, { color: isThinking ? 'yellow' : 'gray', dimColor: !isThinking },
|
||||
isThinking ? '💭 thinking...' : '💭 thinking'
|
||||
),
|
||||
stats.activeAgent && h(Text, { color: 'magenta', marginLeft: 1 }, `(${stats.activeAgent})`),
|
||||
h(Text, { color: 'gray', marginLeft: 1, dimColor: true }, `(${stats.chars} chars)`)
|
||||
),
|
||||
// Thinking lines with cleaner presentation
|
||||
visibleLines.map((line, i) =>
|
||||
h(Text, {
|
||||
key: i,
|
||||
color: 'gray',
|
||||
dimColor: true,
|
||||
wrap: 'truncate'
|
||||
},
|
||||
` ${line.substring(0, width - 4)}` // Cleaner indentation
|
||||
)
|
||||
),
|
||||
// Hidden count indicator
|
||||
hiddenCount > 0 && h(Text, {
|
||||
color: 'gray',
|
||||
dimColor: true,
|
||||
marginLeft: 2
|
||||
},
|
||||
`+${hiddenCount} steps`
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ThinkingBlock;
|
||||
@@ -7,6 +7,7 @@ const h = React.createElement;
|
||||
const TodoList = ({ tasks = [], onAddTask, onCompleteTask, onDeleteTask, width = 60 }) => {
|
||||
const [newTask, setNewTask] = useState('');
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [showCompleted, setShowCompleted] = useState(false); // Toggle to show/hide completed tasks
|
||||
|
||||
const handleAddTask = () => {
|
||||
if (newTask.trim()) {
|
||||
@@ -20,79 +21,196 @@ const TodoList = ({ tasks = [], onAddTask, onCompleteTask, onDeleteTask, width =
|
||||
const completedTasks = tasks.filter(t => t.status === 'completed');
|
||||
const progress = tasks.length > 0 ? Math.round((completedTasks.length / tasks.length) * 100) : 0;
|
||||
|
||||
return h(Box, { flexDirection: 'column', width: width, borderStyle: 'round', borderColor: 'gray', padding: 1 },
|
||||
// Header with title and progress
|
||||
h(Box, { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 1 },
|
||||
h(Text, { bold: true, color: 'white' }, '📋 Tasks'),
|
||||
h(Text, { color: 'cyan' }, `${progress}%`)
|
||||
return h(Box, {
|
||||
flexDirection: 'column',
|
||||
width: width,
|
||||
borderStyle: 'double', // Professional double border
|
||||
borderColor: 'cyan', // Professional accent color
|
||||
paddingX: 1,
|
||||
paddingY: 1,
|
||||
backgroundColor: '#1e1e1e' // Dark theme like professional IDEs
|
||||
},
|
||||
// Header with title, progress, and stats
|
||||
h(Box, {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 1,
|
||||
paddingBottom: 0.5,
|
||||
borderBottom: true,
|
||||
borderColor: 'gray'
|
||||
},
|
||||
h(Text, { bold: true, color: 'cyan' }, '📋 TASK MANAGER'),
|
||||
h(Box, { flexDirection: 'row', gap: 1 },
|
||||
h(Text, { color: 'green' }, `${completedTasks.length}`),
|
||||
h(Text, { color: 'gray' }, '/'),
|
||||
h(Text, { color: 'white' }, `${tasks.length}`),
|
||||
h(Text, { color: 'cyan' }, `(${progress}%)`)
|
||||
)
|
||||
),
|
||||
|
||||
// Progress bar
|
||||
// Progress bar with professional styling
|
||||
h(Box, { marginBottom: 1 },
|
||||
h(Box, {
|
||||
width: width - 4, // Account for padding
|
||||
width: width - 4,
|
||||
height: 1,
|
||||
borderStyle: 'single',
|
||||
borderColor: 'gray',
|
||||
flexDirection: 'row'
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#333333' // Dark background for progress bar
|
||||
},
|
||||
h(Box, {
|
||||
width: Math.max(1, Math.floor((width - 6) * progress / 100)),
|
||||
height: 1,
|
||||
backgroundColor: 'green'
|
||||
backgroundColor: progress === 100 ? 'green' : 'cyan' // Color based on completion
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
// Add new task
|
||||
h(Box, { marginBottom: 1 },
|
||||
// Add new task with enhanced UI
|
||||
h(Box, {
|
||||
marginBottom: 1,
|
||||
paddingX: 0.5,
|
||||
backgroundColor: '#2a2a2a',
|
||||
borderStyle: 'round',
|
||||
borderColor: 'gray'
|
||||
},
|
||||
isAdding
|
||||
? h(Box, { flexDirection: 'row', alignItems: 'center' },
|
||||
h(Text, { color: 'green', marginRight: 1 }, '●'),
|
||||
h(Text, { color: 'green', marginRight: 1 }, '✓'),
|
||||
h(Box, { flexGrow: 1 },
|
||||
h(TextInput, {
|
||||
value: newTask,
|
||||
onChange: setNewTask,
|
||||
onSubmit: handleAddTask,
|
||||
placeholder: 'Add new task...'
|
||||
placeholder: 'Enter new task...',
|
||||
backgroundColor: '#333333'
|
||||
})
|
||||
)
|
||||
)
|
||||
: h(Box, { flexDirection: 'row', alignItems: 'center' },
|
||||
h(Text, { color: 'green', marginRight: 1 }, '➕'),
|
||||
h(Text, { color: 'gray', dimColor: true, onClick: () => setIsAdding(true) }, 'Add task')
|
||||
: h(Box, {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
onClick: () => setIsAdding(true)
|
||||
},
|
||||
h(Text, { color: 'green', marginRight: 1 }, '✚'),
|
||||
h(Text, { color: 'gray', dimColor: false }, 'Add new task (click to add)')
|
||||
)
|
||||
),
|
||||
|
||||
// Tasks list
|
||||
// Tasks list with enhanced styling
|
||||
h(Box, { flexDirection: 'column', flexGrow: 1 },
|
||||
// Pending tasks
|
||||
pendingTasks.map((task, index) =>
|
||||
h(Box, {
|
||||
key: task.id || index,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 0.5
|
||||
},
|
||||
h(Box, {
|
||||
width: 2,
|
||||
height: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'gray',
|
||||
marginRight: 1,
|
||||
onClick: () => onCompleteTask && onCompleteTask(task.id)
|
||||
},
|
||||
h(Text, { color: 'gray' }, '○')
|
||||
),
|
||||
h(Box, { flexGrow: 1 },
|
||||
h(Text, { color: 'white' }, task.content)
|
||||
// Pending tasks section
|
||||
pendingTasks.length > 0
|
||||
? h(Box, { marginBottom: 1 },
|
||||
h(Text, { color: 'yellow', bold: true, marginBottom: 0.5 }, `⚡ ${pendingTasks.length} PENDING`),
|
||||
...pendingTasks.map((task, index) =>
|
||||
h(Box, {
|
||||
key: task.id || index,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 0.5,
|
||||
paddingX: 1,
|
||||
backgroundColor: '#252525',
|
||||
borderStyle: 'single',
|
||||
borderColor: 'gray'
|
||||
},
|
||||
// Complete button
|
||||
h(Box, {
|
||||
width: 3,
|
||||
height: 1,
|
||||
marginRight: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
onClick: () => onCompleteTask && onCompleteTask(task.id),
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
h(Text, { color: 'yellow' }, '○')
|
||||
),
|
||||
// Task content
|
||||
h(Box, { flexGrow: 1 },
|
||||
h(Text, { color: 'white' }, task.content)
|
||||
),
|
||||
// Delete button
|
||||
h(Box, {
|
||||
width: 3,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
onClick: () => onDeleteTask && onDeleteTask(task.id)
|
||||
},
|
||||
h(Text, { color: 'red' }, '✕')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
: h(Text, { color: 'gray', italic: true, marginBottom: 1, marginLeft: 1 }, 'No pending tasks'),
|
||||
|
||||
// Completed tasks (show collapsed by default)
|
||||
// Completed tasks section with toggle
|
||||
completedTasks.length > 0 && h(Box, { marginTop: 1 },
|
||||
h(Text, { color: 'gray', dimColor: true, bold: true }, `✓ ${completedTasks.length} completed`)
|
||||
h(Box, {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
onClick: () => setShowCompleted(!showCompleted)
|
||||
},
|
||||
h(Text, {
|
||||
color: showCompleted ? 'green' : 'gray',
|
||||
bold: true
|
||||
}, `✓ ${completedTasks.length} COMPLETED ${showCompleted ? '−' : '+'}`),
|
||||
h(Text, { color: 'gray', dimColor: true }, showCompleted ? 'click to collapse' : 'click to expand')
|
||||
),
|
||||
showCompleted && h(Box, { marginTop: 0.5 },
|
||||
...completedTasks.map((task, index) =>
|
||||
h(Box, {
|
||||
key: `completed-${task.id || index}`,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 0.5,
|
||||
paddingX: 1,
|
||||
backgroundColor: '#2a2a2a',
|
||||
borderStyle: 'single',
|
||||
borderColor: 'green'
|
||||
},
|
||||
// Completed indicator
|
||||
h(Box, {
|
||||
width: 3,
|
||||
height: 1,
|
||||
marginRight: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
h(Text, { color: 'green', bold: true }, '✓')
|
||||
),
|
||||
// Task content
|
||||
h(Box, { flexGrow: 1 },
|
||||
h(Text, {
|
||||
color: 'gray',
|
||||
strikethrough: true,
|
||||
dimColor: true
|
||||
}, task.content)
|
||||
),
|
||||
// Delete button
|
||||
h(Box, {
|
||||
width: 3,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
onClick: () => onDeleteTask && onDeleteTask(task.id)
|
||||
},
|
||||
h(Text, { color: 'red' }, '✕')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Footer with instructions
|
||||
h(Box, {
|
||||
marginTop: 1,
|
||||
paddingTop: 0.5,
|
||||
borderTop: true,
|
||||
borderColor: 'gray'
|
||||
},
|
||||
h(Text, { color: 'gray', dimColor: true, size: 'small' },
|
||||
'Click ○ to complete • Click ✕ to delete'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user