1147 lines
48 KiB
JavaScript
1147 lines
48 KiB
JavaScript
// OpenQode TUI - Clean interface with numbered selection
|
||
const readline = require('readline');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// Suppress console output from qwen-oauth
|
||
const _log = console.log;
|
||
console.log = () => { };
|
||
console.error = () => { };
|
||
|
||
// Lazy load qwen
|
||
let qwen = null;
|
||
function getQwen() {
|
||
if (!qwen) {
|
||
const { QwenOAuth } = require('../qwen-oauth.cjs');
|
||
qwen = new QwenOAuth();
|
||
}
|
||
return qwen;
|
||
}
|
||
|
||
const print = (...args) => _log.apply(console, args);
|
||
|
||
// ANSI
|
||
const c = {
|
||
reset: '\x1b[0m',
|
||
bold: '\x1b[1m',
|
||
dim: '\x1b[2m',
|
||
cyan: '\x1b[36m',
|
||
green: '\x1b[32m',
|
||
yellow: '\x1b[33m',
|
||
magenta: '\x1b[35m',
|
||
white: '\x1b[97m'
|
||
};
|
||
|
||
// State
|
||
let agent = 'build';
|
||
let selectingAgent = false;
|
||
let agentList = [];
|
||
const agentDir = path.join(__dirname, '..', '.opencode', 'agent');
|
||
let conversationHistory = [];
|
||
const HISTORY_LIMIT = 15; // Keep last 15 turns to maintain context without overflowing
|
||
let useSmartContext = true; // State for Context Toggle
|
||
let exposedThinking = false; // Show all thinking lines when true
|
||
let useCodeCards = true; // Smart Code Presentation Layer
|
||
let codeCards = []; // Store parsed code blocks as cards
|
||
|
||
// Code Card Class for Smart Code Presentation
|
||
class CodeCard {
|
||
constructor(id, language, filename, content) {
|
||
this.id = id;
|
||
this.language = language || 'text';
|
||
this.filename = filename || `snippet_${id}`;
|
||
this.content = content;
|
||
this.lines = content.split('\n').length;
|
||
this.expanded = false;
|
||
}
|
||
|
||
renderCollapsed() {
|
||
const maxWidth = 55;
|
||
const header = `📄 ${this.filename}`;
|
||
const meta = `${this.lines} lines │ ${this.language}`;
|
||
return `
|
||
${c.dim}┌─ ${c.cyan}${header}${c.dim} ${'─'.repeat(Math.max(0, maxWidth - header.length - 4))}┐${c.reset}
|
||
${c.dim}│ ${meta.padEnd(maxWidth - 2)} │${c.reset}
|
||
${c.dim}│ ${c.yellow}[${this.id}]${c.dim} Expand ${c.yellow}[${this.id}c]${c.dim} Copy ${c.yellow}[${this.id}w]${c.dim} Write${''.padEnd(maxWidth - 32)} │${c.reset}
|
||
${c.dim}└${'─'.repeat(maxWidth)}┘${c.reset}`;
|
||
}
|
||
|
||
renderExpanded() {
|
||
const maxWidth = 60;
|
||
const header = `📄 ${this.filename}`;
|
||
const preview = this.content.split('\n').slice(0, 15).map(l =>
|
||
`${c.dim}│${c.reset} ${l.substring(0, maxWidth - 4).padEnd(maxWidth - 4)} ${c.dim}│${c.reset}`
|
||
).join('\n');
|
||
const more = this.lines > 15 ? `\n${c.dim}│ ... ${this.lines - 15} more lines ...${' '.repeat(maxWidth - 22 - String(this.lines - 15).length)}│${c.reset}` : '';
|
||
|
||
return `
|
||
${c.dim}╔═ ${c.cyan}${c.bold}${header}${c.reset}${c.dim} ${'═'.repeat(Math.max(0, maxWidth - header.length - 4))}╗${c.reset}
|
||
${c.dim}║ ${c.green}${this.language}${c.dim} │ ${this.lines} lines${' '.repeat(maxWidth - this.language.length - String(this.lines).length - 12)}║${c.reset}
|
||
${c.dim}╠${'═'.repeat(maxWidth)}╣${c.reset}
|
||
${preview}${more}
|
||
${c.dim}╠${'═'.repeat(maxWidth)}╣${c.reset}
|
||
${c.dim}║ ${c.yellow}[${this.id}]${c.dim} Collapse ${c.yellow}[${this.id}c]${c.dim} Copy ${c.yellow}[${this.id}w]${c.dim} Write File${' '.repeat(maxWidth - 38)}║${c.reset}
|
||
${c.dim}╚${'═'.repeat(maxWidth)}╝${c.reset}`;
|
||
}
|
||
|
||
render() {
|
||
return this.expanded ? this.renderExpanded() : this.renderCollapsed();
|
||
}
|
||
}
|
||
|
||
// Dynamic session log path based on current project
|
||
function getSessionLogFile() {
|
||
return path.join(currentProject || process.cwd(), '.opencode', 'session_log.md');
|
||
}
|
||
|
||
// Log interaction to file for context persistence
|
||
function logInteraction(user, assistant) {
|
||
try {
|
||
const logFile = getSessionLogFile();
|
||
const dir = path.dirname(logFile);
|
||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||
const time = new Date().toISOString().split('T')[1].split('.')[0];
|
||
const entry = `\n### [${time}] User:\n${user}\n\n### Assistant:\n${assistant}\n`;
|
||
fs.appendFileSync(logFile, entry);
|
||
} catch (e) { }
|
||
}
|
||
|
||
// Project Manager
|
||
const RECENT_PROJECTS_FILE = path.join(__dirname, '..', '.opencode', 'recent_projects.json');
|
||
let currentProject = process.cwd();
|
||
|
||
function loadRecentProjects() {
|
||
try {
|
||
if (fs.existsSync(RECENT_PROJECTS_FILE)) {
|
||
return JSON.parse(fs.readFileSync(RECENT_PROJECTS_FILE, 'utf8'));
|
||
}
|
||
} catch (e) { }
|
||
return [];
|
||
}
|
||
|
||
function saveRecentProject(projectPath) {
|
||
try {
|
||
let recent = loadRecentProjects();
|
||
// Remove if exists, add to front
|
||
recent = recent.filter(p => p !== projectPath);
|
||
recent.unshift(projectPath);
|
||
// Keep max 5
|
||
recent = recent.slice(0, 5);
|
||
const dir = path.dirname(RECENT_PROJECTS_FILE);
|
||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||
fs.writeFileSync(RECENT_PROJECTS_FILE, JSON.stringify(recent, null, 2));
|
||
} catch (e) { }
|
||
}
|
||
|
||
function setWorkspace(projectPath) {
|
||
try {
|
||
process.chdir(projectPath);
|
||
currentProject = projectPath;
|
||
saveRecentProject(projectPath);
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Smart Code Presentation - Parse response and render code blocks as cards
|
||
function renderWithCodeCards(text) {
|
||
if (!useCodeCards) return text;
|
||
|
||
codeCards = []; // Reset cards
|
||
let cardId = 1;
|
||
|
||
// Parse code blocks and create cards
|
||
const codeBlockRegex = /```(\w+)?(?:[:\s]+)?([^\n`]+\.\w+)?\n([\s\S]*?)```/g;
|
||
|
||
const rendered = text.replace(codeBlockRegex, (match, lang, filename, content) => {
|
||
const card = new CodeCard(cardId++, lang || 'code', filename || `snippet_${cardId}`, content.trim());
|
||
codeCards.push(card);
|
||
return card.render();
|
||
});
|
||
|
||
return rendered;
|
||
}
|
||
|
||
// Agentic File Operations - Extract code blocks and write files
|
||
function extractCodeBlocks(text) {
|
||
const blocks = [];
|
||
// Match ```filename.ext or ```language:filename.ext or ```language filename.ext
|
||
const regex = /```(?:(\w+)[:\s]+)?([^\n`]+\.\w+)?\n([\s\S]*?)```/g;
|
||
let match;
|
||
|
||
while ((match = regex.exec(text)) !== null) {
|
||
const language = match[1] || '';
|
||
let filename = match[2] || '';
|
||
const content = match[3] || '';
|
||
|
||
// Try to extract filename from first line comment if not in header
|
||
if (!filename && content) {
|
||
const firstLine = content.split('\n')[0];
|
||
const fileMatch = firstLine.match(/(?:\/\/|#|\/\*)\s*(?:file:|filename:)?\s*([^\s*\/]+\.\w+)/i);
|
||
if (fileMatch) {
|
||
filename = fileMatch[1];
|
||
}
|
||
}
|
||
|
||
if (filename && content.trim()) {
|
||
blocks.push({ filename: filename.trim(), content: content.trim(), language });
|
||
}
|
||
}
|
||
return blocks;
|
||
}
|
||
|
||
async function processFileOperations(response, rl) {
|
||
const blocks = extractCodeBlocks(response);
|
||
if (blocks.length === 0) return;
|
||
|
||
print(`\n${c.yellow}📁 Detected ${blocks.length} file(s) to create:${c.reset}`);
|
||
blocks.forEach((b, i) => print(` ${c.cyan}${i + 1}.${c.reset} ${b.filename}`));
|
||
|
||
return new Promise((resolve) => {
|
||
rl.question(`\n${c.cyan}Write files to project?${c.reset} [Y/n/select]: `, async (answer) => {
|
||
const choice = answer.trim().toLowerCase();
|
||
|
||
if (choice === 'n' || choice === 'no') {
|
||
print(`${c.dim} Skipped file creation.${c.reset}\n`);
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
// Write all or selected
|
||
const toWrite = (choice === '' || choice === 'y' || choice === 'yes')
|
||
? blocks
|
||
: blocks.filter((_, i) => choice.includes(String(i + 1)));
|
||
|
||
for (const block of toWrite) {
|
||
try {
|
||
// Handle absolute vs relative paths
|
||
let filePath;
|
||
if (path.isAbsolute(block.filename)) {
|
||
// Absolute path - use directly but warn user
|
||
filePath = block.filename;
|
||
} else {
|
||
// Relative path - join with project directory
|
||
filePath = path.join(currentProject, block.filename);
|
||
}
|
||
|
||
const dir = path.dirname(filePath);
|
||
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
|
||
fs.writeFileSync(filePath, block.content);
|
||
print(` ${c.green}✓${c.reset} Created: ${c.bold}${block.filename}${c.reset}`);
|
||
} catch (err) {
|
||
print(` ${c.yellow}⚠${c.reset} Failed: ${block.filename} - ${err.message}`);
|
||
}
|
||
}
|
||
print('');
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Agentic Command Execution - Run shell commands with user confirmation
|
||
const { spawn, exec } = require('child_process');
|
||
let backgroundProcesses = [];
|
||
|
||
function extractCommands(text) {
|
||
const commands = [];
|
||
// Match ```bash:run or ```shell:run or ```cmd:run or just ```bash with a command
|
||
const regex = /```(?:bash|shell|cmd|sh|powershell|ps1)(?::run)?[\s\n]+([\s\S]*?)```/gi;
|
||
let match;
|
||
|
||
while ((match = regex.exec(text)) !== null) {
|
||
const content = match[1].trim();
|
||
if (content) {
|
||
// Split multiple commands
|
||
content.split('\n').forEach(line => {
|
||
const cmd = line.trim();
|
||
if (cmd && !cmd.startsWith('#')) {
|
||
commands.push(cmd);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
return commands;
|
||
}
|
||
|
||
async function processCommands(response, rl) {
|
||
const commands = extractCommands(response);
|
||
if (commands.length === 0) return;
|
||
|
||
print(`\n${c.magenta}🖥️ Commands detected:${c.reset}`);
|
||
commands.forEach((cmd, i) => print(` ${c.cyan}${i + 1}.${c.reset} ${c.dim}${cmd}${c.reset}`));
|
||
|
||
return new Promise((resolve) => {
|
||
rl.question(`\n${c.cyan}Execute?${c.reset} [Y/n/select/bg]: `, async (answer) => {
|
||
const choice = answer.trim().toLowerCase();
|
||
|
||
if (choice === 'n' || choice === 'no') {
|
||
print(`${c.dim} Skipped command execution.${c.reset}\n`);
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const runInBackground = choice === 'bg' || choice === 'background';
|
||
const toRun = (choice === '' || choice === 'y' || choice === 'yes' || runInBackground)
|
||
? commands
|
||
: commands.filter((_, i) => choice.includes(String(i + 1)));
|
||
|
||
for (const cmd of toRun) {
|
||
try {
|
||
print(`\n ${c.cyan}▶${c.reset} Running: ${c.bold}${cmd}${c.reset}`);
|
||
print(`${c.dim} ───────────────────────────────────────${c.reset}`);
|
||
|
||
if (runInBackground) {
|
||
// Background execution
|
||
const proc = spawn(cmd, [], {
|
||
shell: true,
|
||
cwd: currentProject,
|
||
detached: true,
|
||
stdio: ['ignore', 'pipe', 'pipe']
|
||
});
|
||
|
||
backgroundProcesses.push({ cmd, proc, pid: proc.pid });
|
||
print(` ${c.green}✓${c.reset} Started in background (PID: ${proc.pid})`);
|
||
print(` ${c.dim}Use /ps to see running processes${c.reset}`);
|
||
} else {
|
||
// Foreground execution with live output
|
||
await new Promise((cmdResolve) => {
|
||
const proc = spawn(cmd, [], {
|
||
shell: true,
|
||
cwd: currentProject,
|
||
stdio: ['inherit', 'pipe', 'pipe']
|
||
});
|
||
|
||
proc.stdout.on('data', (data) => {
|
||
const lines = data.toString().split('\n');
|
||
lines.forEach(line => {
|
||
if (line.trim()) print(` ${c.dim}${line}${c.reset}`);
|
||
});
|
||
});
|
||
|
||
proc.stderr.on('data', (data) => {
|
||
const lines = data.toString().split('\n');
|
||
lines.forEach(line => {
|
||
if (line.trim()) print(` ${c.yellow}${line}${c.reset}`);
|
||
});
|
||
});
|
||
|
||
proc.on('close', (code) => {
|
||
if (code === 0) {
|
||
print(` ${c.green}✓${c.reset} Completed (exit: ${code})`);
|
||
} else {
|
||
print(` ${c.yellow}⚠${c.reset} Exited with code: ${code}`);
|
||
}
|
||
cmdResolve();
|
||
});
|
||
|
||
proc.on('error', (err) => {
|
||
print(` ${c.yellow}⚠${c.reset} Error: ${err.message}`);
|
||
cmdResolve();
|
||
});
|
||
});
|
||
}
|
||
} catch (err) {
|
||
print(` ${c.yellow}⚠${c.reset} Failed: ${err.message}`);
|
||
}
|
||
}
|
||
print(`${c.dim} ───────────────────────────────────────${c.reset}\n`);
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
function getAgents() {
|
||
const list = ['build', 'plan'];
|
||
try {
|
||
if (fs.existsSync(agentDir)) {
|
||
fs.readdirSync(agentDir)
|
||
.filter(f => f.endsWith('.md'))
|
||
.forEach(f => list.push(path.basename(f, '.md')));
|
||
}
|
||
} catch (e) { }
|
||
return [...new Set(list)];
|
||
}
|
||
|
||
function loadAgentPrompt(agentName) {
|
||
// 1. Try to load specific agent file
|
||
try {
|
||
const p = path.join(agentDir, `${agentName}.md`);
|
||
if (fs.existsSync(p)) {
|
||
return fs.readFileSync(p, 'utf8');
|
||
}
|
||
} catch (e) { }
|
||
|
||
// Context awareness instruction (shared by all agents)
|
||
const contextInstruction = `
|
||
IMPORTANT: You have access to the PROJECT CONTEXT and SESSION LOG below. Use this information!
|
||
- If there's a SESSION LOG, you know what we discussed before. Reference it naturally.
|
||
- If the user says "continue" or "resume", pick up exactly where the session log left off.
|
||
- Never ask "what project" or "where is it located" if context files are provided.
|
||
|
||
═══════════════════════════════════════════════════════════════
|
||
CLAUDE CODE COMMUNICATION STYLE - Follow this workflow EXACTLY:
|
||
═══════════════════════════════════════════════════════════════
|
||
|
||
1. START with a STATUS UPDATE:
|
||
📋 **Current Task:** [what you're working on]
|
||
📂 **Project:** [project name from context]
|
||
|
||
2. Before EACH major step, ANNOUNCE what you're doing:
|
||
→ Creating project structure...
|
||
→ Setting up database schema...
|
||
→ Installing dependencies...
|
||
|
||
3. After EACH step, CONFIRM completion with checkmarks:
|
||
✓ Created src/App.tsx
|
||
✓ Created src/components/Header.tsx
|
||
✓ Updated task.md
|
||
|
||
4. CONSULT the user before major decisions:
|
||
⚠️ **Decision needed:** Should we use MongoDB or PostgreSQL?
|
||
📊 My recommendation: PostgreSQL (better for relational data)
|
||
👉 Reply 'y' to proceed or tell me your preference.
|
||
|
||
5. UPDATE task.md with progress:
|
||
- [x] Completed items get checked
|
||
- [/] In-progress items
|
||
- [ ] Pending items
|
||
|
||
6. SUMMARIZE after completing a phase:
|
||
───────────────────────────
|
||
✅ **Phase Complete: Project Setup**
|
||
Created: 5 files
|
||
Next: Database schema
|
||
───────────────────────────
|
||
|
||
7. PROPOSE tech stack with rationale table:
|
||
| Component | Choice | Why |
|
||
|-----------|--------|-----|
|
||
| Frontend | React + Vite | Fast HMR, modern |
|
||
| Backend | Express | Simple, flexible |
|
||
| Database | PostgreSQL | Reliable, scales |
|
||
|
||
👉 Proceed with this? (y/n)
|
||
|
||
NEVER ask vague questions. ALWAYS propose specific solutions.
|
||
ALWAYS update task.md as you work. Mark items [x] when done.
|
||
Shall I proceed with this setup?"
|
||
|
||
FILE CREATION: When writing code, use this format so files can be auto-created:
|
||
\`\`\`javascript:src/index.js
|
||
// your code here
|
||
\`\`\`
|
||
The format is: \`\`\`language:path/to/filename.ext
|
||
Always include the full relative path. The user will be prompted to confirm file creation.
|
||
|
||
COMMAND EXECUTION: When you need to run commands (npm, docker, git, etc.), use:
|
||
\`\`\`bash
|
||
npm install
|
||
npm run dev
|
||
\`\`\`
|
||
Commands will be shown to the user for confirmation before executing. You can run multiple commands.
|
||
For long-running commands (servers), the user can choose to run in background.
|
||
`;
|
||
|
||
// 2. Fallback defaults
|
||
if (agentName === 'plan') {
|
||
return `You are the PLAN agent. Your job is to analyze requests and create detailed architectural plans.
|
||
- Break down projects into clear phases and tasks.
|
||
- ALWAYS propose a complete technology stack with your recommendations.
|
||
- Create detailed task lists and file structures.
|
||
- DO NOT ask what technologies to use - PROPOSE them with brief justifications.
|
||
- After presenting the plan, ask "Shall I proceed?" or "Want me to adjust anything?"
|
||
|
||
DOCUMENT YOUR DECISIONS in task.md like this:
|
||
## Decisions & Rationale
|
||
- **React** - Modern, component-based, huge ecosystem
|
||
- **PostgreSQL** - ACID compliance, scales well, free
|
||
- **JWT Auth** - Stateless, works with APIs
|
||
|
||
${contextInstruction}`;
|
||
}
|
||
return `You are the BUILD agent. Your job is to build projects from start to finish.
|
||
- You are a senior full-stack developer helping a beginner.
|
||
- ALWAYS propose complete solutions with specific technologies.
|
||
- DO NOT ask questions about tech stack - RECOMMEND and explain your choices briefly.
|
||
- Start building immediately after user confirms your proposed plan.
|
||
- Create files, install dependencies, and set up the project structure proactively.
|
||
|
||
EXPLAIN YOUR CHOICES: For EVERY major decision, briefly explain WHY:
|
||
- "Using React because it's component-based and has great tooling"
|
||
- "Chose PostgreSQL for reliability and JSON support"
|
||
- "Using JWT tokens for stateless authentication"
|
||
|
||
UPDATE task.md with a "Decisions" section documenting your rationale:
|
||
## Decisions & Rationale
|
||
| Choice | Reason |
|
||
|--------|--------|
|
||
| React + Vite | Fast dev server, modern tooling |
|
||
| Material-UI | Professional look, less CSS work |
|
||
| PostgreSQL | Reliable, scales well, free |
|
||
${contextInstruction}`;
|
||
}
|
||
|
||
function getProjectContext() {
|
||
let context = "";
|
||
|
||
// 1. Load Session History (The "Context Manager" / "Shared Brain")
|
||
const logFile = getSessionLogFile();
|
||
if (fs.existsSync(logFile)) {
|
||
try {
|
||
const logContent = fs.readFileSync(logFile, 'utf8');
|
||
// Keep last 20KB to avoid overflowing context
|
||
const MAX_LOG_SIZE = 20000;
|
||
if (logContent.length > MAX_LOG_SIZE) {
|
||
context += `\n[PAST SESSION LOG (Truncated)]\n...${logContent.slice(-MAX_LOG_SIZE)}\n`;
|
||
} else {
|
||
context += `\n[FULL SESSION LOG]\n${logContent}\n`;
|
||
}
|
||
} catch (e) { }
|
||
}
|
||
|
||
// 2. Check for common context files in current directory
|
||
const files = ['task.md', 'implementation_plan.md', 'TODO.md', 'README.md'];
|
||
files.forEach(f => {
|
||
if (fs.existsSync(f)) {
|
||
const content = fs.readFileSync(f, 'utf8');
|
||
// Limit context size per file
|
||
if (content.length < 5000) {
|
||
context += `\n--- FILE: ${f} ---\n${content}\n`;
|
||
} else {
|
||
context += `\n--- FILE: ${f} (Truncated) ---\n${content.substring(0, 5000)}\n...`;
|
||
}
|
||
}
|
||
});
|
||
return context;
|
||
}
|
||
|
||
function showHeader() {
|
||
print('\x1b[2J\x1b[H');
|
||
print(`${c.dim}───────────────────────────────────────────────────────${c.reset}`);
|
||
print(` ${c.bold}${c.cyan}◆ OpenQode v1.2 Alpha${c.reset} ${c.dim}AI Coding Assistant${c.reset}`);
|
||
print(`${c.dim}───────────────────────────────────────────────────────${c.reset}`);
|
||
print(` Agent: ${c.bold}${c.cyan}${agent}${c.reset} ${c.dim}│${c.reset} ${c.dim}/help for commands${c.reset}`);
|
||
print(` ${c.dim}Project:${c.reset} ${c.bold}${path.basename(currentProject)}${c.reset}`);
|
||
print(`${c.dim}───────────────────────────────────────────────────────${c.reset}\n`);
|
||
|
||
// Show short history summary
|
||
if (conversationHistory.length > 0) {
|
||
print(`${c.dim} History: ${conversationHistory.length} messages loaded${c.reset}`);
|
||
}
|
||
|
||
// Show Smart Context Status
|
||
const ctxStatus = useSmartContext ? `${c.green}[ON]${c.reset}` : `${c.dim}[OFF]${c.reset}`;
|
||
let ctxSize = "0B";
|
||
const logFile = getSessionLogFile();
|
||
if (useSmartContext && fs.existsSync(logFile)) {
|
||
const stats = fs.statSync(logFile);
|
||
ctxSize = (stats.size / 1024).toFixed(1) + "KB";
|
||
}
|
||
print(` ${c.bold}Smart Context:${c.reset} ${ctxStatus} ${c.dim}(${ctxSize})${c.reset}\n`);
|
||
}
|
||
|
||
// Show context summary after project load
|
||
function showContextSummary() {
|
||
const logFile = getSessionLogFile();
|
||
if (fs.existsSync(logFile)) {
|
||
try {
|
||
const content = fs.readFileSync(logFile, 'utf8');
|
||
if (content.length > 100) {
|
||
// Extract last few lines to show what was discussed
|
||
const lines = content.split('\n').filter(l => l.trim());
|
||
const lastLines = lines.slice(-10).join('\n').substring(0, 500);
|
||
|
||
print(`${c.dim}───────────────────────────────────────────────────────${c.reset}`);
|
||
print(` ${c.bold}${c.green}📚 Session Restored!${c.reset}`);
|
||
print(` ${c.dim}I remember our previous conversation.${c.reset}`);
|
||
print(` ${c.dim}Just say "continue" or tell me what's next.${c.reset}`);
|
||
print(`${c.dim}───────────────────────────────────────────────────────${c.reset}\n`);
|
||
} else {
|
||
print(` ${c.dim}New session. What would you like to work on?${c.reset}\n`);
|
||
}
|
||
} catch (e) { }
|
||
} else {
|
||
// Check for task.md to understand project
|
||
if (fs.existsSync('task.md') || fs.existsSync('README.md')) {
|
||
print(` ${c.dim}Project files detected. Tell me what you'd like to do!${c.reset}\n`);
|
||
} else {
|
||
print(` ${c.dim}New project. What would you like to build?${c.reset}\n`);
|
||
}
|
||
}
|
||
}
|
||
|
||
function showAgentMenu() {
|
||
agentList = getAgents();
|
||
selectingAgent = true;
|
||
print(`\n${c.bold} Select Agent${c.reset} ${c.dim}(enter number or 0 to cancel)${c.reset}\n`);
|
||
agentList.forEach((a, i) => {
|
||
const current = a === agent ? `${c.green}●${c.reset}` : `${c.dim}○${c.reset}`;
|
||
print(` ${current} ${c.cyan}${i + 1}${c.reset} ${a === agent ? c.white + c.bold : c.dim}${a}${c.reset}`);
|
||
});
|
||
print('');
|
||
}
|
||
|
||
const rl = readline.createInterface({
|
||
input: process.stdin,
|
||
output: process.stdout
|
||
});
|
||
|
||
// Startup: Project Selection Menu
|
||
function showProjectMenu() {
|
||
print('\x1b[2J\x1b[H');
|
||
print(`${c.dim}───────────────────────────────────────────────────────${c.reset}`);
|
||
print(` ${c.bold}${c.cyan}◆ OpenQode v1.2 Alpha${c.reset} ${c.dim}AI Coding Assistant${c.reset}`);
|
||
print(`${c.dim}───────────────────────────────────────────────────────${c.reset}\n`);
|
||
|
||
print(` ${c.bold}Choose your workspace:${c.reset}\n`);
|
||
|
||
const recent = loadRecentProjects();
|
||
const cwd = process.cwd();
|
||
let options = [];
|
||
|
||
// Option 1: Current directory
|
||
print(` ${c.cyan}1${c.reset} ${c.dim}Current directory${c.reset}`);
|
||
print(` ${c.dim}${cwd}${c.reset}`);
|
||
options.push({ type: 'cwd', path: cwd });
|
||
|
||
// Recent projects (up to 3)
|
||
let optNum = 2;
|
||
recent.slice(0, 3).forEach(p => {
|
||
if (p !== cwd && fs.existsSync(p)) {
|
||
print(` ${c.cyan}${optNum}${c.reset} ${c.dim}Recent:${c.reset} ${path.basename(p)}`);
|
||
print(` ${c.dim}${p}${c.reset}`);
|
||
options.push({ type: 'recent', path: p });
|
||
optNum++;
|
||
}
|
||
});
|
||
|
||
// Browse option
|
||
print(` ${c.cyan}${optNum}${c.reset} ${c.yellow}Browse / Enter a path...${c.reset}`);
|
||
options.push({ type: 'browse' });
|
||
optNum++;
|
||
|
||
// Create new
|
||
print(` ${c.cyan}${optNum}${c.reset} ${c.green}Create new project${c.reset}`);
|
||
options.push({ type: 'new' });
|
||
|
||
print('');
|
||
|
||
rl.question(` ${c.cyan}Enter choice:${c.reset} `, (choice) => {
|
||
const num = parseInt(choice);
|
||
if (num >= 1 && num <= options.length) {
|
||
const opt = options[num - 1];
|
||
if (opt.type === 'cwd' || opt.type === 'recent') {
|
||
if (setWorkspace(opt.path)) {
|
||
print(`\n${c.green}✓${c.reset} Workspace: ${c.bold}${opt.path}${c.reset}\n`);
|
||
showHeader();
|
||
showContextSummary(); // Show what we remember
|
||
prompt();
|
||
} else {
|
||
print(`\n${c.yellow}⚠${c.reset} Could not access: ${opt.path}\n`);
|
||
showProjectMenu();
|
||
}
|
||
} else if (opt.type === 'browse') {
|
||
rl.question(` ${c.cyan}Enter full path:${c.reset} `, (customPath) => {
|
||
if (customPath.trim() && fs.existsSync(customPath.trim())) {
|
||
setWorkspace(customPath.trim());
|
||
print(`\n${c.green}✓${c.reset} Workspace: ${c.bold}${customPath.trim()}${c.reset}\n`);
|
||
showHeader();
|
||
prompt();
|
||
} else {
|
||
print(`\n${c.yellow}⚠${c.reset} Path not found.\n`);
|
||
showProjectMenu();
|
||
}
|
||
});
|
||
} else if (opt.type === 'new') {
|
||
rl.question(` ${c.cyan}New project path:${c.reset} `, (newPath) => {
|
||
if (newPath.trim()) {
|
||
try {
|
||
fs.mkdirSync(newPath.trim(), { recursive: true });
|
||
fs.writeFileSync(path.join(newPath.trim(), 'task.md'), '# Project Task List\n\n- [ ] Define project goals\n');
|
||
setWorkspace(newPath.trim());
|
||
print(`\n${c.green}✓${c.reset} Created: ${c.bold}${newPath.trim()}${c.reset}\n`);
|
||
showHeader();
|
||
prompt();
|
||
} catch (e) {
|
||
print(`\n${c.yellow}⚠${c.reset} Could not create: ${e.message}\n`);
|
||
showProjectMenu();
|
||
}
|
||
} else {
|
||
showProjectMenu();
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
showProjectMenu();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Start with project menu
|
||
showProjectMenu();
|
||
|
||
function prompt() {
|
||
const promptStr = selectingAgent ? `${c.cyan}#${c.reset} ` : `${c.green}❯${c.reset} `;
|
||
|
||
rl.question(promptStr, async (input) => {
|
||
const text = input.trim();
|
||
|
||
// Agent selection mode
|
||
if (selectingAgent) {
|
||
const num = parseInt(text);
|
||
if (num === 0 || text === '' || text.toLowerCase() === 'q') {
|
||
selectingAgent = false;
|
||
print(`${c.dim} Cancelled${c.reset}\n`);
|
||
} else if (num >= 1 && num <= agentList.length) {
|
||
agent = agentList[num - 1];
|
||
selectingAgent = false;
|
||
print(`\n${c.green}✓${c.reset} Selected: ${c.bold}${c.cyan}${agent}${c.reset}\n`);
|
||
showHeader();
|
||
} else {
|
||
print(`${c.dim} Invalid. Enter 1-${agentList.length} or 0 to cancel${c.reset}\n`);
|
||
}
|
||
prompt();
|
||
return;
|
||
}
|
||
|
||
if (!text) { prompt(); return; }
|
||
|
||
if (text.startsWith('/')) {
|
||
handleCommand(text);
|
||
return;
|
||
}
|
||
|
||
// Construct Stateful Prompt
|
||
const systemPrompt = loadAgentPrompt(agent);
|
||
let projectContext = "";
|
||
if (useSmartContext) {
|
||
projectContext = getProjectContext();
|
||
}
|
||
|
||
let historyStr = "";
|
||
if (useSmartContext) {
|
||
conversationHistory.forEach(msg => {
|
||
historyStr += `\n${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}\n`;
|
||
});
|
||
}
|
||
|
||
const fullPrompt = `[SYSTEM INSTRUCTIONS]
|
||
${systemPrompt}
|
||
|
||
[PROJECT CONTEXT]
|
||
${projectContext}
|
||
|
||
[CONVERSATION HISTORY]
|
||
${historyStr}
|
||
|
||
[CURRENT REQUEST]
|
||
User: ${text}
|
||
Assistant:`;
|
||
|
||
// Live Thinking Indicator
|
||
let elapsed = 0;
|
||
const thinkingInterval = setInterval(() => {
|
||
elapsed++;
|
||
process.stdout.write(`\r${c.dim} ● thinking... (${elapsed}s)${c.reset}`);
|
||
}, 1000);
|
||
print(`${c.dim} ● thinking... (0s)${c.reset}`);
|
||
|
||
// Smart Retry Logic
|
||
const MAX_RETRIES = 2;
|
||
let attempt = 0;
|
||
let success = false;
|
||
let lastError = null;
|
||
|
||
while (attempt <= MAX_RETRIES && !success) {
|
||
attempt++;
|
||
try {
|
||
// Streaming: display output as it arrives
|
||
let streamStarted = false;
|
||
let fullResponse = '';
|
||
let thinkingLines = [];
|
||
let lastThinkingCount = 0;
|
||
let inThinkingBlock = false;
|
||
|
||
// Patterns that indicate "thinking" output
|
||
const thinkingPatterns = [
|
||
/^(Let me|Now let me|I'll|I need to|I notice|I should|Wait,|Now I)/i,
|
||
/^(Checking|Looking|Analyzing|Creating|Updating|Setting up)/i
|
||
];
|
||
|
||
const isThinkingLine = (line) => {
|
||
return thinkingPatterns.some(pattern => pattern.test(line.trim()));
|
||
};
|
||
|
||
const result = await getQwen().sendMessage(fullPrompt, 'qwen-coder-plus', null, (chunk) => {
|
||
// First chunk - clear thinking indicator and show start
|
||
if (!streamStarted) {
|
||
clearInterval(thinkingInterval);
|
||
process.stdout.write('\r\x1b[2K'); // Clear the thinking line
|
||
print(`\n${c.cyan}◆${c.reset} `);
|
||
streamStarted = true;
|
||
}
|
||
|
||
// Clean ANSI codes
|
||
const cleanChunk = chunk.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||
fullResponse += cleanChunk;
|
||
|
||
// Check each line for thinking patterns
|
||
const lines = cleanChunk.split('\n');
|
||
for (const line of lines) {
|
||
if (isThinkingLine(line)) {
|
||
thinkingLines.push(line.trim());
|
||
|
||
if (exposedThinking) {
|
||
// Exposed mode: show all thinking lines
|
||
process.stdout.write(`${c.dim}${line.trim()}${c.reset}\n`);
|
||
} else {
|
||
// Rolling window: show last 4 lines
|
||
const windowSize = 4;
|
||
const recentLines = thinkingLines.slice(-windowSize);
|
||
const clearLines = '\x1b[2K\x1b[1A'.repeat(Math.min(thinkingLines.length - 1, windowSize));
|
||
if (thinkingLines.length > 1) process.stdout.write(clearLines);
|
||
process.stdout.write(`${c.dim}💭 Thinking (${thinkingLines.length} steps):\n`);
|
||
recentLines.forEach(l => {
|
||
process.stdout.write(` ${l.substring(0, 70)}${l.length > 70 ? '...' : ''}\n`);
|
||
});
|
||
process.stdout.write(c.reset);
|
||
}
|
||
inThinkingBlock = true;
|
||
} else if (line.trim()) {
|
||
// Non-thinking content: show it
|
||
if (inThinkingBlock) {
|
||
process.stdout.write('\r\x1b[2K'); // Clear the thinking line
|
||
inThinkingBlock = false;
|
||
}
|
||
process.stdout.write(line);
|
||
if (!cleanChunk.endsWith(line)) process.stdout.write('\n');
|
||
} else {
|
||
// Empty lines
|
||
if (!inThinkingBlock) process.stdout.write('\n');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Store thinking for /expand command
|
||
if (thinkingLines.length > 0) {
|
||
global.lastThinking = thinkingLines;
|
||
if (inThinkingBlock) {
|
||
process.stdout.write(`\r\x1b[2K${c.dim}💭 ${thinkingLines.length} thinking steps (type /expand to see)${c.reset}\n`);
|
||
}
|
||
}
|
||
|
||
if (!streamStarted) {
|
||
clearInterval(thinkingInterval);
|
||
process.stdout.write('\r\x1b[2K'); // Clear the thinking line
|
||
}
|
||
|
||
if (result.success) {
|
||
// If streaming didn't happen (fallback), show full response
|
||
if (!streamStarted && result.response) {
|
||
// Render with code cards if enabled
|
||
const displayResponse = renderWithCodeCards(result.response);
|
||
print(`\n${c.cyan}◆${c.reset} ${displayResponse}\n`);
|
||
} else {
|
||
// For streamed content, re-render with cards at the end
|
||
if (useCodeCards && fullResponse) {
|
||
const cardDisplay = renderWithCodeCards(fullResponse);
|
||
// Only show card summary if cards were found
|
||
if (codeCards.length > 0) {
|
||
print(`\n${c.dim}📦 ${codeCards.length} code card(s) - use /card <n> to expand, /card <n>c to copy${c.reset}`);
|
||
}
|
||
}
|
||
print('\n'); // Add newline after streamed content
|
||
}
|
||
|
||
// Update History
|
||
conversationHistory.push({ role: 'user', content: text });
|
||
conversationHistory.push({ role: 'assistant', content: result.response });
|
||
|
||
// Persist to context file
|
||
if (useSmartContext) {
|
||
logInteraction(text, result.response);
|
||
}
|
||
|
||
// Trim history
|
||
if (conversationHistory.length > HISTORY_LIMIT * 2) {
|
||
conversationHistory = conversationHistory.slice(-(HISTORY_LIMIT * 2));
|
||
}
|
||
|
||
// Agentic: Process any file creation requests
|
||
await processFileOperations(result.response, rl);
|
||
|
||
// Agentic: Process any command execution requests
|
||
await processCommands(result.response, rl);
|
||
|
||
success = true;
|
||
} else {
|
||
lastError = result.error;
|
||
if (attempt <= MAX_RETRIES) {
|
||
process.stdout.write(`\r${c.yellow}⚠ Attempt ${attempt} failed. Retrying...${c.reset}`);
|
||
await new Promise(r => setTimeout(r, 1000 * attempt)); // Backoff
|
||
}
|
||
}
|
||
} catch (err) {
|
||
clearInterval(thinkingInterval);
|
||
lastError = err.message;
|
||
if (attempt <= MAX_RETRIES && (err.message.includes('timeout') || err.message.includes('ETIMEDOUT'))) {
|
||
process.stdout.write(`\r${c.yellow}⚠ Timeout. Retrying (${attempt}/${MAX_RETRIES})...${c.reset}\n`);
|
||
await new Promise(r => setTimeout(r, 2000 * attempt)); // Longer backoff for timeout
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!success) {
|
||
process.stdout.write('\r\x1b[2K');
|
||
print(`\n${c.yellow}⚠${c.reset} ${lastError || 'Request failed.'}`);
|
||
print(`${c.dim} Tip: Try a shorter prompt or check your connection.${c.reset}\n`);
|
||
}
|
||
|
||
prompt();
|
||
});
|
||
}
|
||
|
||
function handleCommand(text) {
|
||
const parts = text.split(' ');
|
||
const cmd = parts[0].toLowerCase();
|
||
|
||
switch (cmd) {
|
||
case '/exit':
|
||
case '/quit':
|
||
case '/q':
|
||
print(`\n${c.dim}Goodbye!${c.reset}\n`);
|
||
process.exit(0);
|
||
break;
|
||
|
||
case '/clear':
|
||
case '/c':
|
||
showHeader();
|
||
break;
|
||
|
||
case '/new':
|
||
print(`\n${c.green}✓${c.reset} ${c.dim}New conversation${c.reset}\n`);
|
||
codeCards = []; // Reset code cards
|
||
break;
|
||
|
||
case '/cards':
|
||
useCodeCards = !useCodeCards;
|
||
print(`\n${c.green}✓${c.reset} Code Cards: ${useCodeCards ? c.bold + 'ON' : c.dim + 'OFF'}${c.reset}\n`);
|
||
break;
|
||
|
||
case '/card':
|
||
if (parts.length < 2) {
|
||
// List all cards
|
||
if (codeCards.length === 0) {
|
||
print(`\n${c.dim}No code cards available. Send a message to get code first.${c.reset}\n`);
|
||
} else {
|
||
print(`\n${c.bold}📦 Code Cards:${c.reset}`);
|
||
codeCards.forEach(card => print(card.render()));
|
||
print('');
|
||
}
|
||
} else {
|
||
// Parse card ID and action
|
||
const arg = parts[1];
|
||
const cardId = parseInt(arg);
|
||
const action = arg.replace(/\d+/, ''); // Extract action (c, w, e)
|
||
|
||
const card = codeCards.find(c => c.id === (action ? parseInt(arg) : cardId));
|
||
if (!card) {
|
||
print(`\n${c.dim}Card ${cardId || arg} not found.${c.reset}\n`);
|
||
} else if (action === 'c' || parts[2] === 'copy') {
|
||
// Copy to clipboard
|
||
try {
|
||
require('child_process').execSync(
|
||
process.platform === 'win32'
|
||
? `echo ${card.content.replace(/"/g, '\\"')} | clip`
|
||
: `echo "${card.content}" | pbcopy || xclip -selection clipboard`,
|
||
{ stdio: 'pipe' }
|
||
);
|
||
print(`\n${c.green}✓${c.reset} Copied card ${card.id} to clipboard\n`);
|
||
} catch (e) {
|
||
print(`\n${c.yellow}⚠${c.reset} Clipboard copy failed. Content:\n${c.dim}${card.content.substring(0, 200)}...${c.reset}\n`);
|
||
}
|
||
} else if (action === 'w' || parts[2] === 'write') {
|
||
// Write to file
|
||
try {
|
||
const filePath = path.isAbsolute(card.filename)
|
||
? card.filename
|
||
: path.join(currentProject, card.filename);
|
||
const dir = path.dirname(filePath);
|
||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||
fs.writeFileSync(filePath, card.content);
|
||
print(`\n${c.green}✓${c.reset} Created: ${c.bold}${card.filename}${c.reset}\n`);
|
||
} catch (e) {
|
||
print(`\n${c.yellow}⚠${c.reset} Failed to write: ${e.message}\n`);
|
||
}
|
||
} else {
|
||
// Toggle expand/collapse
|
||
card.expanded = !card.expanded;
|
||
print(card.render());
|
||
}
|
||
}
|
||
break;
|
||
|
||
case '/expand':
|
||
case '/thinking':
|
||
if (parts[1] === 'on') {
|
||
exposedThinking = true;
|
||
print(`\n${c.green}✓${c.reset} Exposed Thinking: ${c.bold}ON${c.reset} - See all AI reasoning\n`);
|
||
} else if (parts[1] === 'off') {
|
||
exposedThinking = false;
|
||
print(`\n${c.green}✓${c.reset} Exposed Thinking: ${c.dim}OFF${c.reset} - Rolling 4-line window\n`);
|
||
} else if (global.lastThinking && global.lastThinking.length > 0) {
|
||
print(`\n${c.bold}💭 Last Thinking Process (${global.lastThinking.length} steps):${c.reset}`);
|
||
print(`${c.dim}───────────────────────────────────────${c.reset}`);
|
||
global.lastThinking.forEach((step, i) => {
|
||
print(`${c.dim}${i + 1}. ${step}${c.reset}`);
|
||
});
|
||
print(`${c.dim}───────────────────────────────────────${c.reset}`);
|
||
print(`\n${c.dim}Tip: /thinking on|off to toggle exposed mode${c.reset}\n`);
|
||
} else {
|
||
print(`\n${c.dim}Exposed Thinking: ${exposedThinking ? 'ON' : 'OFF'}`);
|
||
print(`Usage: /thinking on|off${c.reset}\n`);
|
||
}
|
||
break;
|
||
|
||
case '/context':
|
||
case '/brain':
|
||
useSmartContext = !useSmartContext;
|
||
showHeader();
|
||
print(`\n${c.green}✓${c.reset} Smart Context is now ${useSmartContext ? c.bold + "ON" : c.dim + "OFF"}${c.reset}\n`);
|
||
break;
|
||
|
||
case '/project':
|
||
case '/workspace':
|
||
showProjectMenu();
|
||
return; // Don't call prompt() here, showProjectMenu handles it
|
||
|
||
case '/agents':
|
||
case '/a':
|
||
showAgentMenu();
|
||
break;
|
||
|
||
case '/ps':
|
||
case '/processes':
|
||
if (backgroundProcesses.length === 0) {
|
||
print(`\n${c.dim} No background processes running.${c.reset}\n`);
|
||
} else {
|
||
print(`\n${c.bold} Background Processes:${c.reset}`);
|
||
backgroundProcesses.forEach((p, i) => {
|
||
const alive = !p.proc.killed;
|
||
print(` ${c.cyan}${i + 1}.${c.reset} [${alive ? c.green + 'RUNNING' : c.dim + 'STOPPED'}${c.reset}] PID:${p.pid} - ${c.dim}${p.cmd}${c.reset}`);
|
||
});
|
||
print('');
|
||
}
|
||
break;
|
||
|
||
case '/kill':
|
||
if (parts[1]) {
|
||
const idx = parseInt(parts[1]) - 1;
|
||
if (backgroundProcesses[idx]) {
|
||
try {
|
||
process.kill(backgroundProcesses[idx].pid);
|
||
print(`\n${c.green}✓${c.reset} Killed process ${backgroundProcesses[idx].pid}\n`);
|
||
} catch (e) {
|
||
print(`\n${c.yellow}⚠${c.reset} ${e.message}\n`);
|
||
}
|
||
} else {
|
||
print(`\n${c.yellow}⚠${c.reset} Invalid process number. Use /ps to list.\n`);
|
||
}
|
||
} else {
|
||
print(`\n${c.dim} Usage: /kill <number> - Use /ps to see process list.${c.reset}\n`);
|
||
}
|
||
break;
|
||
|
||
case '/run':
|
||
const cmdToRun = parts.slice(1).join(' ');
|
||
if (cmdToRun) {
|
||
print(`\n ${c.cyan}▶${c.reset} Running: ${c.bold}${cmdToRun}${c.reset}`);
|
||
exec(cmdToRun, { cwd: currentProject }, (err, stdout, stderr) => {
|
||
if (stdout) print(`${c.dim}${stdout}${c.reset}`);
|
||
if (stderr) print(`${c.yellow}${stderr}${c.reset}`);
|
||
if (err) print(`${c.yellow}⚠ Error: ${err.message}${c.reset}`);
|
||
prompt();
|
||
});
|
||
return;
|
||
} else {
|
||
print(`\n${c.dim} Usage: /run <command>${c.reset}\n`);
|
||
}
|
||
break;
|
||
|
||
// Update help menu separately
|
||
case '/clear':
|
||
case '/c':
|
||
showHeader();
|
||
break;
|
||
|
||
case '/add':
|
||
print(`\n${c.bold} Create New Agent${c.reset}\n`);
|
||
rl.question(` ${c.cyan}Name${c.reset} (lowercase, no spaces): `, (name) => {
|
||
if (!name.trim()) {
|
||
print(`${c.dim} Cancelled${c.reset}\n`);
|
||
prompt();
|
||
return;
|
||
}
|
||
const safeName = name.trim().toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
|
||
rl.question(` ${c.cyan}Purpose${c.reset} (what should it do?): `, (purpose) => {
|
||
if (!purpose.trim()) {
|
||
print(`${c.dim} Cancelled${c.reset}\n`);
|
||
prompt();
|
||
return;
|
||
}
|
||
// Create agent file
|
||
const agentPath = path.join(agentDir, `${safeName}.md`);
|
||
const content = `# ${safeName.charAt(0).toUpperCase() + safeName.slice(1)} Agent\n\n${purpose.trim()}\n`;
|
||
try {
|
||
if (!fs.existsSync(agentDir)) {
|
||
fs.mkdirSync(agentDir, { recursive: true });
|
||
}
|
||
fs.writeFileSync(agentPath, content);
|
||
print(`\n${c.green}✓${c.reset} Created: ${c.cyan}${safeName}${c.reset}`);
|
||
print(`${c.dim} File: .opencode/agent/${safeName}.md${c.reset}\n`);
|
||
agent = safeName;
|
||
showHeader();
|
||
} catch (err) {
|
||
print(`\n${c.yellow}⚠${c.reset} Failed: ${err.message}\n`);
|
||
}
|
||
prompt();
|
||
});
|
||
});
|
||
return;
|
||
|
||
case '/help':
|
||
case '/?':
|
||
print(`
|
||
${c.bold} Commands${c.reset}
|
||
${c.cyan}/project${c.reset} ${c.dim}Switch workspace / project${c.reset}
|
||
${c.cyan}/context${c.reset} ${c.dim}Toggle Smart Context (Brain)${c.reset}
|
||
${c.cyan}/thinking${c.reset} ${c.dim}Toggle exposed thinking mode (on|off)${c.reset}
|
||
${c.cyan}/expand${c.reset} ${c.dim}View last thinking process${c.reset}
|
||
${c.cyan}/cards${c.reset} ${c.dim}Toggle code card presentation${c.reset}
|
||
${c.cyan}/card${c.reset} ${c.dim}List code cards or /card <n> to expand${c.reset}
|
||
${c.cyan}/agents${c.reset} ${c.dim}Browse and select agents${c.reset}
|
||
${c.cyan}/add${c.reset} ${c.dim}Create a new agent${c.reset}
|
||
${c.cyan}/run${c.reset} ${c.dim}Execute a shell command${c.reset}
|
||
${c.cyan}/ps${c.reset} ${c.dim}List background processes${c.reset}
|
||
${c.cyan}/kill${c.reset} ${c.dim}Stop a background process${c.reset}
|
||
${c.cyan}/new${c.reset} ${c.dim}Start fresh conversation${c.reset}
|
||
${c.cyan}/clear${c.reset} ${c.dim}Clear screen${c.reset}
|
||
${c.cyan}/exit${c.reset} ${c.dim}Quit${c.reset}
|
||
`);
|
||
break;
|
||
|
||
default:
|
||
print(`\n${c.dim} Unknown command. Try /help${c.reset}\n`);
|
||
}
|
||
prompt();
|
||
}
|
||
|
||
prompt();
|