feat: enhance AI communication with dynamic system prompts, robust retry, and TUI formatters

This commit is contained in:
Gemini AI
2025-12-14 22:16:52 +04:00
Unverified
parent 61b72bcd5f
commit a8436c91a3
20 changed files with 9832 additions and 808 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View File

@@ -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'
)
)
);