TUI Gen 5: Major overhaul - Todo List, improved streaming, ESC to abort, cleaner UI

This commit is contained in:
Gemini AI
2025-12-14 04:02:03 +04:00
Unverified
parent e23a2a5efc
commit 63de8fc2d1
6 changed files with 5417 additions and 688 deletions

File diff suppressed because it is too large Load Diff

4101
bin/opencode-ink.mjs.backup Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -683,7 +683,40 @@ function showProjectMenu() {
} }
// Start with project menu // Start with project menu
// Start with auth check then project menu
(async () => {
const qwen = getQwen();
const authed = await qwen.checkAuth();
if (!authed) {
print(`\n${c.yellow}Authentication required. Launching web login...${c.reset}\n`);
const authScript = path.join(__dirname, 'auth.js');
await new Promise((resolve) => {
const child = spawn('node', [authScript], {
stdio: 'inherit',
shell: false
});
child.on('close', (code) => {
if (code === 0) {
print(`\n${c.green}Authentication successful! Starting TUI...${c.reset}\n`);
resolve();
} else {
print(`\n${c.red}Authentication failed or was cancelled.${c.reset}\n`);
process.exit(1);
}
});
});
// Re-check auth
const recheck = await qwen.checkAuth();
if (!recheck) {
process.exit(1);
}
}
showProjectMenu(); showProjectMenu();
})();
function prompt() { function prompt() {
const promptStr = selectingAgent ? `${c.cyan}#${c.reset} ` : `${c.green}${c.reset} `; const promptStr = selectingAgent ? `${c.cyan}#${c.reset} ` : `${c.green}${c.reset} `;

View File

@@ -4,16 +4,21 @@ import { Box, Text } from 'ink';
const h = React.createElement; const h = React.createElement;
const ChatBubble = ({ role, content, meta, width, children }) => { 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 (RIGHT ALIGNED) - RAIL STYLE // USER MESSAGE - Clean text-focused presentation
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
if (role === 'user') { if (role === 'user') {
return h(Box, { width: width, flexDirection: 'row', justifyContent: 'flex-end', marginBottom: 1, overflow: 'hidden' }, return h(Box, {
h(Box, { flexDirection: 'row', paddingRight: 1 }, width: width,
h(Text, { color: 'cyan', wrap: 'wrap' }, content), flexDirection: 'row',
h(Box, { marginLeft: 1, borderStyle: 'single', borderLeft: false, borderTop: false, borderBottom: false, borderRightColor: 'cyan' }) justifyContent: 'flex-end',
) marginBottom: 1,
paddingLeft: 2
},
h(Text, { color: 'cyan', wrap: 'wrap' }, content)
); );
} }
@@ -27,27 +32,39 @@ const ChatBubble = ({ role, content, meta, width, children }) => {
} }
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// ERROR - RED GUTTER // ERROR - CLEAN GUTTER STYLE
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
if (role === 'error') { if (role === 'error') {
// Strip redundant "Error: " prefix if present in content // Strip redundant "Error: " prefix if present in content
const cleanContent = content.replace(/^Error:\s*/i, ''); const cleanContent = content.replace(/^Error:\s*/i, '');
return h(Box, { width: width, flexDirection: 'row', marginBottom: 1, overflow: 'hidden' }, return h(Box, {
h(Box, { marginRight: 1, borderStyle: 'single', borderRight: false, borderTop: false, borderBottom: false, borderLeftColor: 'red' }), width: width,
flexDirection: 'row',
marginBottom: 1
},
h(Box, { width: 1, marginRight: 1, backgroundColor: 'red' }),
h(Text, { color: 'red', wrap: 'wrap' }, cleanContent) h(Text, { color: 'red', wrap: 'wrap' }, cleanContent)
); );
} }
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// ASSISTANT - LEFT GUTTER RAIL // ASSISTANT - Clean text-focused style (Opencode-like)
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
return h(Box, { width: width, flexDirection: 'row', marginBottom: 1, overflow: 'hidden' }, return h(Box, {
// Left Gutter width: width,
h(Box, { marginRight: 1, borderStyle: 'single', borderRight: false, borderTop: false, borderBottom: false, borderLeftColor: 'green' }), 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 // Content area - text focused, no borders
h(Box, { flexDirection: 'column', paddingRight: 2, flexGrow: 1 }, h(Box, {
children ? children : h(Text, { wrap: 'wrap' }, content) flexDirection: 'column',
flexGrow: 1,
minWidth: 10
},
children ? children : h(Text, { color: 'white', wrap: 'wrap' }, content)
) )
); );
}; };

View File

@@ -13,28 +13,54 @@ const ThinkingBlock = ({
if (lines.length === 0 && !isThinking) return null; if (lines.length === 0 && !isThinking) return null;
// Show only last few lines to avoid clutter // Show only last few lines to avoid clutter
const visibleLines = lines.slice(-3); const visibleLines = lines.slice(-3); // Show cleaner view
const hiddenCount = Math.max(0, lines.length - 3); const hiddenCount = Math.max(0, lines.length - 3);
return h(Box, { return h(Box, {
flexDirection: 'row', flexDirection: 'row',
width: width, width: width,
marginBottom: 1, marginBottom: 1,
overflow: 'hidden' paddingLeft: 1 // Only left padding, no borders like opencode
}, },
// Left Gutter (Dimmed) // Clean left gutter similar to opencode
h(Box, { marginRight: 1, borderStyle: 'single', borderRight: false, borderTop: false, borderBottom: false, borderLeftColor: 'gray', borderDimColor: true }), h(Box, {
width: 2,
marginRight: 1,
borderStyle: 'single',
borderRight: false,
borderTop: false,
borderBottom: false,
borderLeftColor: isThinking ? 'yellow' : 'gray'
}),
h(Box, { flexDirection: 'column' }, h(Box, { flexDirection: 'column', flexGrow: 1 },
h(Text, { color: 'gray', dimColor: true }, // Header with minimal stats - opencode style
isThinking h(Box, { marginBottom: 0.5, flexDirection: 'row' },
? `🧠 Thinking${stats.activeAgent ? ` (${stats.activeAgent})` : ''}... (${stats.chars} chars)` h(Text, { color: isThinking ? 'yellow' : 'gray', dimColor: !isThinking },
: `💭 Thought Process (${stats.chars} chars)` 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) => visibleLines.map((line, i) =>
h(Text, { key: i, color: 'gray', dimColor: true, wrap: 'truncate' }, ` ${line}`) h(Text, {
key: i,
color: 'gray',
dimColor: true,
wrap: 'truncate'
},
` ${line.substring(0, width - 4)}` // Cleaner indentation
)
), ),
hiddenCount > 0 ? h(Text, { color: 'gray', dimColor: true, italic: true }, ` ...${hiddenCount} more`) : null // Hidden count indicator
hiddenCount > 0 && h(Text, {
color: 'gray',
dimColor: true,
marginLeft: 2
},
`+${hiddenCount} steps`
)
) )
); );
}; };

View File

@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import { Box, Text } from 'ink';
import TextInput from 'ink-text-input';
const h = React.createElement;
const TodoList = ({ tasks = [], onAddTask, onCompleteTask, onDeleteTask, width = 60 }) => {
const [newTask, setNewTask] = useState('');
const [isAdding, setIsAdding] = useState(false);
const handleAddTask = () => {
if (newTask.trim()) {
onAddTask && onAddTask(newTask.trim());
setNewTask('');
setIsAdding(false);
}
};
const pendingTasks = tasks.filter(t => t.status !== 'completed');
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}%`)
),
// Progress bar
h(Box, { marginBottom: 1 },
h(Box, {
width: width - 4, // Account for padding
height: 1,
borderStyle: 'single',
borderColor: 'gray',
flexDirection: 'row'
},
h(Box, {
width: Math.max(1, Math.floor((width - 6) * progress / 100)),
height: 1,
backgroundColor: 'green'
})
)
),
// Add new task
h(Box, { marginBottom: 1 },
isAdding
? h(Box, { flexDirection: 'row', alignItems: 'center' },
h(Text, { color: 'green', marginRight: 1 }, '●'),
h(Box, { flexGrow: 1 },
h(TextInput, {
value: newTask,
onChange: setNewTask,
onSubmit: handleAddTask,
placeholder: 'Add new task...'
})
)
)
: h(Box, { flexDirection: 'row', alignItems: 'center' },
h(Text, { color: 'green', marginRight: 1 }, ''),
h(Text, { color: 'gray', dimColor: true, onClick: () => setIsAdding(true) }, 'Add task')
)
),
// Tasks list
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)
)
)
),
// Completed tasks (show collapsed by default)
completedTasks.length > 0 && h(Box, { marginTop: 1 },
h(Text, { color: 'gray', dimColor: true, bold: true }, `${completedTasks.length} completed`)
)
)
);
};
export default TodoList;