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

@@ -0,0 +1,100 @@
# TUI 5 Feature Enhancements - Implementation Plan
## Overview
Implementing 5 features inspired by Mini-Agent concepts, written as **100% original code** for TUI 5's React Ink architecture.
---
## Phase 1: Persistent Session Memory 🥇
### Files to Create
- `lib/session-memory.mjs` - SessionMemory class
### Implementation
```javascript
// lib/session-memory.mjs
class SessionMemory {
constructor() {
this.memoryFile = '.openqode-memory.json';
this.facts = [];
}
async load() { /* Load from JSON file */ }
async save() { /* Save to JSON file */ }
async remember(fact) { /* Add fact with timestamp */ }
async forget(index) { /* Remove fact by index */ }
getContext() { /* Return facts as system prompt addition */ }
}
```
### Commands
- `/remember <fact>` - Save important context
- `/forget <index>` - Remove a remembered fact
- `/memory` - Show all remembered facts
---
## Phase 2: Intelligent Context Summarization 🥈
### Files to Create
- `lib/context-manager.mjs` - ContextManager class
### Implementation
```javascript
class ContextManager {
constructor(tokenLimit = 100000) {
this.tokenLimit = tokenLimit;
}
countTokens(text) { /* Estimate tokens */ }
shouldSummarize(messages) { /* Check if > 50% limit */ }
async summarize(messages) { /* Call AI to summarize old messages */ }
}
```
### UI Indicator
Show `[Context: 45%]` in stats panel
---
## Phase 3: Skills Library 🥉
### Files to Create
- `skills/index.mjs` - Skills registry
- `skills/definitions/` - Individual skill files
### Built-in Skills
| Skill | Description |
|-------|-------------|
| `pdf` | Generate PDF documentation |
| `test` | Create unit tests |
| `review` | Code review analysis |
| `docs` | Generate documentation |
| `refactor` | Suggest refactoring |
### Commands
- `/skills` - List available skills
- `/skill <name>` - Execute a skill
---
## Phase 4: Request Logging
### Implementation
- Add `--debug` CLI flag
- Create `.openqode-debug.log`
- Log API calls with timestamps
- `/debug` toggle command
---
## Phase 5: MCP Support (Future)
Research and design phase - defer to later sprint.
---
## Verification
- Test each feature in isolation
- Verify no regressions in existing functionality
- Push to GitHub after each phase

View File

@@ -0,0 +1,39 @@
# Implementation Plan - Integrating Enhanced Agent Communication
## Goal Description
Integrate the new `agent-prompt.mjs` module (concise, direct, informative patterns) into the OpenQode TUI. Refactor `server.js` (if applicable) and primarily `bin/opencode-ink.mjs` and `qwen-oauth.mjs` to support dynamic system prompt injection and robust retry mechanisms for API calls.
## User Review Required
> [!IMPORTANT]
> The `qwen-oauth.mjs` `sendMessage` signature will be updated to accept `systemPrompt` as a 5th argument. This is a non-breaking change as it defaults to null, but ensures future compatibility.
## Proposed Changes
### Core Logic
#### [MODIFY] [qwen-oauth.mjs](file:///e:/TRAE%20Playground/Test%20Ideas/OpenQode-v1.01-Preview/qwen-oauth.mjs)
- Update `sendMessage` to accept `systemPrompt` as the 5th argument.
- Use the provided `systemPrompt` instead of the hardcoded `systemContext`.
- Import `fetchWithRetry` from `lib/retry-handler.mjs` (module import).
- Wrap `sendVisionMessage`'s `fetch` call with `fetchWithRetry`.
#### [MODIFY] [bin/opencode-ink.mjs](file:///e:/TRAE%20Playground/Test%20Ideas/OpenQode-v1.01-Preview/bin/opencode-ink.mjs)
- Import `getSystemPrompt` from `../lib/agent-prompt.mjs`.
- Import `fetchWithRetry` from `../lib/retry-handler.mjs` (for `callOpenCodeFree`).
- In `handleSubmit`:
- Gather context (CWD, project context, memories).
- Call `getSystemPrompt({ capabilities, cwd, context, projectContext })` to generate the cleaner prompt.
- Pass this `systemPrompt` to `qwen.sendMessage` as the 5th argument.
- PASS ONLY the user request (and maybe immediate context like "clipboard content") as the message content, removing the manual prompt concatenation.
- In `callOpenCodeFree`:
- Use `fetchWithRetry` instead of raw `fetch`.
## Verification Plan
### Automated Tests
- None available for TUI interaction.
### Manual Verification
1. **System Prompt Check**: Send a message like "create a file test.txt". Verify the agent responds concisely (OpenCode style) and uses the correct code block format, proving `getSystemPrompt` was used.
2. **Retry Check**: Disconnect internet (if possible) or simulate a timeout to verify `fetchWithRetry` logs attempts and handles failure gracefully.
3. **Vision Check**: Send an image command (if possible via TUI) to verify `sendVisionMessage` still works with retry.

20
.opencode/task.md Normal file
View File

@@ -0,0 +1,20 @@
# Task: Enhance AI Communication Patterns
## Objectives
- [x] Integrate `agent-prompt.mjs` for dynamic system prompts
- [x] Implement `fetchWithRetry` for robust API calls
- [x] Enhance TUI message rendering with `message-renderer.mjs` formatters
## Progress
- [x] Create Implementation Plan
- [x] Backup `qwen-oauth.mjs` and `bin/opencode-ink.mjs`
- [x] Update `qwen-oauth.mjs`:
- [x] Import `fetchWithRetry`
- [x] Add `systemPrompt` support to `sendMessage`
- [x] Wrap `sendVisionMessage` with retry logic
- [x] Update `bin/opencode-ink.mjs`:
- [x] Import `getSystemPrompt` and `fetchWithRetry`
- [x] Refactor `handleSubmit` to use dynamic system prompt
- [x] Update `callOpenCodeFree` to use `fetchWithRetry`
- [x] Apply `formatSuccess`/`formatError` to file save output
- [ ] User Verification of functionality

37
.opencode/walkthrough.md Normal file
View File

@@ -0,0 +1,37 @@
# Walkthrough: Enhanced Agent Communication
I have successfully integrated the enhanced system prompt, retry mechanism, and TUI formatters.
## Changes Applied
### 1. Robust API Calls (`qwen-oauth.mjs`)
- **Retry Logic**: Integrated `fetchWithRetry` for Vision API calls.
- **Dynamic System Prompt**: `sendMessage` now accepts a `systemPrompt` argument, allowing the TUI to inject context-aware instructions instead of relying on hardcoded overrides.
### 2. TUI Logic (`bin/opencode-ink.mjs`)
- **System Prompt Injection**: `handleSubmit` now generates a clean, role-specific system prompt using `lib/agent-prompt.mjs`.
- **Stream Refactoring**: Unified the streaming callback logic for cleaner code.
- **Retry Integration**: `callOpenCodeFree` now uses `fetchWithRetry` for better resilience.
- **Visual Feedback**: File save operations now use `formatSuccess` and `formatFileOperation` for consistent, bordered output.
## Verification Steps
> [!IMPORTANT]
> You **MUST** restart your TUI process (`node bin/opencode-ink.mjs`) for these changes to take effect.
1. **Restart the TUI**.
2. **Test System Prompt**:
- Send a simple greeting: "Hello".
- **Expected**: A concise, direct response (no "As an AI..." preamble).
- ask "Create a file named `demo.txt` with text 'Hello World'".
- **Expected**: The agent should generate the file using the correct code block format.
3. **Test Visual Feedback**:
- Observe the success message after file creation.
- **Expected**: A green bordered box saying "✅ Success" with the file details.
4. **Test Retry (Optional)**:
- If you can simulate a network glitch, the system should now log "Retrying...".
## Rollback
Backups were created before applying changes:
- `qwen-oauth.mjs.bak`
- `bin/opencode-ink.mjs.bak`

View File

@@ -1,37 +1,119 @@
Write-Host "OpenQode Auto-Installer" -ForegroundColor Cyan
Write-Host "-----------------------" -ForegroundColor Cyan
# OpenQode Auto-Installer for Windows (PowerShell)
# Noob-proof: Auto-installs Node.js and Git if missing!
$ErrorActionPreference = "Stop"
Write-Host ""
Write-Host " ╔═══════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host " ║ 🚀 OpenQode Auto-Installer 🚀 ║" -ForegroundColor Cyan
Write-Host " ║ Next-Gen AI Coding Assistant ║" -ForegroundColor Cyan
Write-Host " ╚═══════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# Function to check if running as admin
function Test-Admin {
$currentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
# Function to install winget package
function Install-WingetPackage($PackageId, $Name) {
Write-Host "[*] Installing $Name..." -ForegroundColor Yellow
try {
winget install --id $PackageId --accept-package-agreements --accept-source-agreements -e
if ($LASTEXITCODE -eq 0) {
Write-Host "[✓] $Name installed successfully!" -ForegroundColor Green
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
return $true
}
} catch {
Write-Host "[!] winget failed, trying alternative method..." -ForegroundColor Yellow
}
return $false
}
# Check for Git
Write-Host "[1/4] Checking for Git..." -ForegroundColor Cyan
if (!(Get-Command git -ErrorAction SilentlyContinue)) {
Write-Host "Error: Git is not installed." -ForegroundColor Red
Write-Host "Please install Git: https://git-scm.com/download/win"
exit
Write-Host "[!] Git not found. Installing..." -ForegroundColor Yellow
$installed = Install-WingetPackage "Git.Git" "Git"
if (!$installed) {
Write-Host "[!] Attempting direct download..." -ForegroundColor Yellow
$gitInstaller = "$env:TEMP\git-installer.exe"
Invoke-WebRequest -Uri "https://github.com/git-for-windows/git/releases/download/v2.43.0.windows.1/Git-2.43.0-64-bit.exe" -OutFile $gitInstaller
Start-Process -FilePath $gitInstaller -Args "/VERYSILENT /NORESTART" -Wait
Remove-Item $gitInstaller -Force
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
}
if (!(Get-Command git -ErrorAction SilentlyContinue)) {
Write-Host "[X] Failed to install Git. Please install manually: https://git-scm.com/download/win" -ForegroundColor Red
exit 1
}
}
Write-Host "[✓] Git is installed!" -ForegroundColor Green
# Check for Node
# Check for Node.js
Write-Host "[2/4] Checking for Node.js..." -ForegroundColor Cyan
if (!(Get-Command node -ErrorAction SilentlyContinue)) {
Write-Host "Error: Node.js is not installed." -ForegroundColor Red
Write-Host "Please install Node.js: https://nodejs.org/"
exit
Write-Host "[!] Node.js not found. Installing..." -ForegroundColor Yellow
$installed = Install-WingetPackage "OpenJS.NodeJS.LTS" "Node.js LTS"
if (!$installed) {
Write-Host "[!] Attempting direct download..." -ForegroundColor Yellow
$nodeInstaller = "$env:TEMP\node-installer.msi"
Invoke-WebRequest -Uri "https://nodejs.org/dist/v20.10.0/node-v20.10.0-x64.msi" -OutFile $nodeInstaller
Start-Process msiexec.exe -Args "/i `"$nodeInstaller`" /qn ADDLOCAL=ALL" -Wait
Remove-Item $nodeInstaller -Force
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
}
if (!(Get-Command node -ErrorAction SilentlyContinue)) {
Write-Host "[X] Failed to install Node.js. Please install manually: https://nodejs.org/" -ForegroundColor Red
exit 1
}
}
$nodeVer = node --version
Write-Host "[✓] Node.js $nodeVer is installed!" -ForegroundColor Green
# Clone or update repository
$repoUrl = "https://github.com/roman-ryzenadvanced/OpenQode-Public-Alpha.git"
$targetDir = "OpenQode"
Write-Host "[3/4] Setting up OpenQode..." -ForegroundColor Cyan
if (Test-Path $targetDir) {
Write-Host "Directory '$targetDir' already exists. Updating..." -ForegroundColor Yellow
Write-Host "[*] Directory exists. Updating..." -ForegroundColor Yellow
Push-Location $targetDir
git pull
git pull --ff-only
Pop-Location
} else {
Write-Host "Cloning repository..." -ForegroundColor Yellow
Write-Host "[*] Cloning repository..." -ForegroundColor Yellow
git clone $repoUrl $targetDir
}
# Install npm dependencies (clean install to ensure React overrides work)
Set-Location $targetDir
Write-Host "[4/4] Installing dependencies..." -ForegroundColor Cyan
# Clean existing node_modules to ensure React overrides take effect
if (Test-Path "node_modules") {
Write-Host "[*] Cleaning existing dependencies for fresh install..." -ForegroundColor Yellow
Remove-Item -Recurse -Force "node_modules" -ErrorAction SilentlyContinue
Remove-Item -Force "package-lock.json" -ErrorAction SilentlyContinue
}
Write-Host "Installing dependencies..." -ForegroundColor Yellow
npm install --legacy-peer-deps
if ($LASTEXITCODE -ne 0) {
Write-Host "[!] npm install failed, retrying..." -ForegroundColor Yellow
npm cache clean --force
npm install --legacy-peer-deps
}
Write-Host "Installation complete! Launching..." -ForegroundColor Green
Write-Host ""
Write-Host " ╔═══════════════════════════════════════════╗" -ForegroundColor Green
Write-Host " ║ ✅ Installation Complete! ✅ ║" -ForegroundColor Green
Write-Host " ║ ║" -ForegroundColor Green
Write-Host " ║ Launching OpenQode Next-Gen TUI... ║" -ForegroundColor Green
Write-Host " ╚═══════════════════════════════════════════╝" -ForegroundColor Green
Write-Host ""
# Launch
.\OpenQode.bat

View File

@@ -114,6 +114,12 @@ We recommend starting with **Next-Gen (Option 5)**!
OpenQode Gen 5 introduces a powerful suite of intelligent automation features designed for maximum productivity:
### 🧠 Core Intelligence v2
- **Dynamic System Prompting**: Context-aware prompts that adapt to your project, OS, and active agents.
- **Robust API Resilience**: Enhanced retry mechanisms for Vision API and network instability.
- **Smart TUI Formatters**: Cleaner, bordered output for file operations and success messages.
![Features Panel](assets/features-smartx.png)
### 🔀 Multi-Agent Mode (`/agents`)

38
add-auto-approve.js Normal file
View File

@@ -0,0 +1,38 @@
const fs = require('fs');
let c = fs.readFileSync('bin/opencode-ink.mjs', 'utf8');
// 1. Add /auto command handler before /theme
const autoCmd = ` case '/auto':
setAutoApprove(prev => !prev);
setMessages(prev => [...prev, {
role: 'system',
content: !autoApprove ? '▶️ Auto-Approve **ENABLED** - Commands execute automatically in SOLO mode' : '⏸ Auto-Approve **DISABLED** - Commands require confirmation'
}]);
setInput('');
return;
`;
// Only add if not already present
if (!c.includes("case '/auto':")) {
c = c.replace(/(case '\/theme':)/g, autoCmd + ' $1');
console.log('Added /auto command handler');
}
// 2. Add useEffect to auto-execute commands when autoApprove is true
const autoExecEffect = `
// AUTO-APPROVE: Automatically execute commands in SOLO mode
useEffect(() => {
if (autoApprove && soloMode && detectedCommands.length > 0 && !isExecutingCommands) {
handleExecuteCommands(true);
}
}, [autoApprove, soloMode, detectedCommands, isExecutingCommands]);
`;
// Insert after soloMode state declaration
if (!c.includes('AUTO-APPROVE: Automatically execute')) {
c = c.replace(/(const \[autoApprove, setAutoApprove\] = useState\(false\);[^\n]*\n)/g, '$1' + autoExecEffect);
console.log('Added auto-execute useEffect');
}
fs.writeFileSync('bin/opencode-ink.mjs', c);
console.log('Done!');

30
add-sidebar-tags.js Normal file
View File

@@ -0,0 +1,30 @@
const fs = require('fs');
let c = fs.readFileSync('bin/opencode-ink.mjs', 'utf8');
// Add SOLO and Auto-Approve indicators after the "Think" row in sidebar
const soloIndicators = ` h(Box, {},
h(Text, { color: 'gray' }, 'SOLO: '),
soloMode
? h(Text, { color: 'magenta', bold: true }, 'ON')
: h(Text, { color: 'gray', dimColor: true }, 'OFF')
),
h(Box, {},
h(Text, { color: 'gray' }, 'AutoRun:'),
autoApprove
? h(Text, { color: 'yellow', bold: true }, 'ON')
: h(Text, { color: 'gray', dimColor: true }, 'OFF')
),`;
// Insert after the Think row (before the empty h(Text, {}, '') line)
if (!c.includes("h(Text, { color: 'gray' }, 'SOLO: ')")) {
c = c.replace(
/(h\(Box, \{\},\s*\n\s*h\(Text, \{ color: 'gray' \}, 'Think: '\),\s*\n\s*exposedThinking\s*\n\s*\? h\(Text, \{ color: 'green', bold: true \}, 'ON'\)\s*\n\s*: h\(Text, \{ color: 'gray', dimColor: true \}, 'OFF'\)\s*\n\s*\),)/g,
'$1\n' + soloIndicators
);
console.log('Added SOLO and Auto-Approve indicators to sidebar');
} else {
console.log('Indicators already present');
}
fs.writeFileSync('bin/opencode-ink.mjs', c);
console.log('Done!');

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

View File

@@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DedicatedNodes.io Presentation</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
height: 100vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.presentation-container {
width: 90%;
max-width: 1200px;
height: 85vh;
background: white;
border-radius: 12px;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.3);
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
.slide-container {
flex: 1;
position: relative;
overflow: hidden;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 40px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
opacity: 0;
transform: translateX(100%);
transition: all 0.5s ease-in-out;
}
.slide.active {
opacity: 1;
transform: translateX(0);
}
.slide.next {
opacity: 0;
transform: translateX(100%);
}
.slide.prev {
opacity: 0;
transform: translateX(-100%);
}
.slide-title {
font-size: 2.5rem;
font-weight: bold;
color: #1a2a6c;
margin-bottom: 20px;
text-align: center;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
.slide-content {
font-size: 1.3rem;
line-height: 1.6;
color: #333;
text-align: center;
max-width: 800px;
}
.highlight {
color: #b21f1f;
font-weight: bold;
}
.navigation {
padding: 15px 20px;
background: #f5f5f5;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #ddd;
}
.nav-button {
background: #1a2a6c;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background 0.3s;
}
.nav-button:hover {
background: #0d1b4d;
}
.nav-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.slide-indicator {
color: #666;
font-size: 1rem;
}
.logo {
font-size: 1.8rem;
font-weight: bold;
color: #1a2a6c;
margin-bottom: 30px;
}
.features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-top: 30px;
width: 100%;
max-width: 600px;
}
.feature-card {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
text-align: center;
border: 1px solid #e0e0e0;
}
.contact-info {
margin-top: 30px;
text-align: center;
}
.contact-link {
color: #1a2a6c;
text-decoration: none;
font-weight: bold;
font-size: 1.2rem;
}
.contact-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="presentation-container">
<div class="slide-container">
<!-- Slide 1: Title Slide -->
<div class="slide active" id="slide1">
<div class="logo">www.dedicatednodes.io</div>
<h1 class="slide-title">Dedicated Nodes</h1>
<p class="slide-content">High-performance dedicated server solutions for businesses and individuals</p>
<p class="slide-content" style="margin-top: 20px;">Professional hosting services with reliable infrastructure</p>
</div>
<!-- Slide 2: About -->
<div class="slide" id="slide2">
<h1 class="slide-title">About DedicatedNodes.io</h1>
<p class="slide-content">DedicatedNodes.io provides premium <span class="highlight">dedicated server hosting</span> solutions designed for:</p>
<div class="features-grid">
<div class="feature-card">Web Hosting</div>
<div class="feature-card">Game Servers</div>
<div class="feature-card">Database Hosting</div>
<div class="feature-card">Enterprise Applications</div>
</div>
</div>
<!-- Slide 3: Services -->
<div class="slide" id="slide3">
<h1 class="slide-title">Our Services</h1>
<p class="slide-content">We offer a range of dedicated server options:</p>
<div class="features-grid">
<div class="feature-card"><span class="highlight">SSD Storage</span><br>Fast storage solutions</div>
<div class="feature-card"><span class="highlight">DDoS Protection</span><br>Advanced security</div>
<div class="feature-card"><span class="highlight">24/7 Support</span><br>Round-the-clock assistance</div>
<div class="feature-card"><span class="highlight">Global Locations</span><br>Multiple data centers</div>
</div>
</div>
<!-- Slide 4: Benefits -->
<div class="slide" id="slide4">
<h1 class="slide-title">Why Choose Us</h1>
<p class="slide-content">Benefits of using DedicatedNodes.io:</p>
<div style="text-align: left; max-width: 700px; margin-top: 20px;">
<p><span class="highlight">Full control</span> over your server resources</p>
<p>• High performance and reliability</p>
<p>• No resource sharing with other users</p>
<p>• Complete customization options</p>
<p>• Competitive pricing plans</p>
<p>• Professional technical support</p>
</div>
</div>
<!-- Slide 5: Contact -->
<div class="slide" id="slide5">
<h1 class="slide-title">Get Started Today</h1>
<p class="slide-content">Ready to experience premium dedicated server hosting?</p>
<div class="contact-info">
<p>Visit us at:</p>
<a href="https://www.dedicatednodes.io" target="_blank" class="contact-link">www.dedicatednodes.io</p>
<p style="margin-top: 20px;">Start your journey with reliable, high-performance servers</p>
</div>
</div>
</div>
<div class="navigation">
<button class="nav-button" id="prevBtn" disabled>Previous</button>
<div class="slide-indicator" id="slideCounter">Slide 1 of 5</div>
<button class="nav-button" id="nextBtn">Next</button>
</div>
</div>
<script>
let currentSlide = 0;
const slides = document.querySelectorAll('.slide');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const slideCounter = document.getElementById('slideCounter');
function showSlide(index) {
// Remove all classes
slides.forEach(slide => {
slide.classList.remove('active', 'next', 'prev');
});
// Add appropriate classes
slides[index].classList.add('active');
// Update button states
prevBtn.disabled = index === 0;
nextBtn.textContent = index === slides.length - 1 ? 'Restart' : 'Next';
// Update counter
slideCounter.textContent = `Slide ${index + 1} of ${slides.length}`;
}
function nextSlide() {
if (currentSlide < slides.length - 1) {
currentSlide++;
showSlide(currentSlide);
} else {
// If we're on the last slide, restart from the beginning
currentSlide = 0;
showSlide(currentSlide);
}
}
function prevSlide() {
if (currentSlide > 0) {
currentSlide--;
showSlide(currentSlide);
}
}
// Event listeners
nextBtn.addEventListener('click', nextSlide);
prevBtn.addEventListener('click', prevSlide);
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') {
nextSlide();
} else if (e.key === 'ArrowLeft') {
prevSlide();
}
});
// Initialize
showSlide(currentSlide);
</script>
</body>
</html>

91
lib/agent-prompt.cjs Normal file
View File

@@ -0,0 +1,91 @@
/**
* Agent Prompt - Enhanced communication patterns for OpenQode TUI (CommonJS Adapter)
* Based on: OpenCode CLI and Mini-Agent best practices
*/
/**
* Get the enhanced system prompt for the AI agent
* @param {Object} context - Context object with project info
* @returns {string} - The complete system prompt
*/
function getSystemPrompt(context = {}) {
const {
projectPath = process.cwd(),
isGitRepo = false,
platform = process.platform,
model = 'unknown',
skills = [],
memory = []
} = context;
const date = new Date().toLocaleDateString();
const memoryContext = memory.length > 0
? `\n## Session Memory\n${memory.map((m, i) => `${i + 1}. ${m}`).join('\n')}\n`
: '';
return `You are OpenQode, an interactive CLI coding assistant that helps users with software engineering tasks.
## Core Behavior
### Tone & Style
- Be CONCISE and DIRECT. Respond in 1-4 lines unless the user asks for detail.
- NO preamble like "Here's what I'll do..." or "Based on my analysis..."
- NO postamble like "Let me know if you need anything else!"
- One-word or short answers when appropriate (e.g., user asks "is X prime?" → "Yes")
- When running commands, briefly explain WHAT it does (not obvious details)
### Response Examples
<example>
User: what's 2+2?
You: 4
</example>
<example>
User: how do I list files?
You: ls
</example>
<example>
User: create a React component for a button
You: [Creates the file directly using tools, then says:]
Created Button.jsx with onClick handler and styling.
</example>
### Code Actions
- When creating/editing files, DO IT directly - don't just show code
- After file operations, give a ONE-LINE summary of what was created
- Use file separators for code blocks:
\`\`\`
┌─ filename.js ──────────────────────────────────
│ code here
└────────────────────────────────────────────────
\`\`\`
### Tool Usage
- If you need information, USE TOOLS to find it - don't guess
- Run lint/typecheck after code changes when available
- Never commit unless explicitly asked
- Explain destructive commands before running them
### Error Handling
- Report errors with: problem + solution
- Format: ❌ Error: [what went wrong] → [how to fix]
## Environment
<env>
Working Directory: ${projectPath}
Git Repository: ${isGitRepo ? 'Yes' : 'No'}
Platform: ${platform}
Model: ${model}
Date: ${date}
</env>
${memoryContext}
## Available Skills
${skills.length > 0 ? skills.map(s => `- ${s.name}: ${s.description}`).join('\n') : 'Use /skills to see available skills'}
Remember: Keep responses SHORT. Act, don't explain. Code directly, summarize briefly.`;
}
module.exports = {
getSystemPrompt
};

View File

@@ -281,17 +281,17 @@ class QwenOAuth {
});
}
async sendMessage(message, model = 'qwen-coder-plus', imageData = null, onChunk = null) {
async sendMessage(message, model = 'qwen-coder-plus', imageData = null, onChunk = null, systemPrompt = null) {
if (imageData) {
console.log('📷 Image data detected, using Vision API...');
return await this.sendVisionMessage(message, imageData, 'qwen-vl-plus');
return await this.sendVisionMessage(message, imageData, 'qwen-vl-plus', systemPrompt);
}
const { spawn } = require('child_process');
const os = require('os');
// fsSync imported at top
const systemContext = `[SYSTEM CONTEXT - ALWAYS FOLLOW]
const hardcodedContext = `[SYSTEM CONTEXT - ALWAYS FOLLOW]
You are an AI System Administrator integrated into OpenQode.
IMPORTANT RULES:
1. You have FULL ACCESS to the local file system.
@@ -304,8 +304,11 @@ IMPORTANT RULES:
`;
let finalMessage = message;
if (message.includes('CREATE:') || message.includes('ROLE:') || message.includes('Generate all necessary files')) {
finalMessage = systemContext + message;
// Use provided systemPrompt if available, otherwise fall back to hardcoded context for legacy commands
if (systemPrompt) {
finalMessage = systemPrompt + "\n\n" + message;
} else if (message.includes('CREATE:') || message.includes('ROLE:') || message.includes('Generate all necessary files')) {
finalMessage = hardcodedContext + message;
}
return new Promise((resolve) => {

View File

@@ -12,6 +12,7 @@ import { readFile, writeFile, unlink } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import { fetchWithRetry } from './lib/retry-handler.mjs';
// ESM __dirname equivalent
const __filename = fileURLToPath(import.meta.url);
@@ -335,8 +336,9 @@ class QwenOAuth {
* @param {string} model - The model to use
* @param {object} imageData - Optional image data
* @param {function} onChunk - Optional callback for streaming output (chunk) => void
* @param {string} systemPrompt - Optional system prompt to override/prepend
*/
async sendMessage(message, model = 'qwen-coder-plus', imageData = null, onChunk = null) {
async sendMessage(message, model = 'qwen-coder-plus', imageData = null, onChunk = null, systemPrompt = null) {
// If we have image data, always use the Vision API
if (imageData) {
console.log('📷 Image data detected, using Vision API...');
@@ -348,8 +350,14 @@ class QwenOAuth {
const os = await import('os');
const fsSync = await import('fs');
// CRITICAL: Prepend system context to prevent AI from getting confused about environment
const systemContext = `[SYSTEM CONTEXT - ALWAYS FOLLOW]
let finalMessage = message;
// If systemPrompt is provided (New Flow), use it directly + message
if (systemPrompt) {
finalMessage = systemPrompt + '\n\n' + message;
} else {
// Legacy Flow: Prepend hardcoded context for specific keywords
const systemContext = `[SYSTEM CONTEXT - ALWAYS FOLLOW]
You are an AI System Administrator integrated into OpenQode.
IMPORTANT RULES:
1. You have FULL ACCESS to the local file system.
@@ -360,17 +368,15 @@ IMPORTANT RULES:
[END SYSTEM CONTEXT]
`;
// Prepend system context ONLY for build/create commands (detected by keywords)
let finalMessage = message;
const lowerMsg = message.toLowerCase();
if (message.includes('CREATE:') ||
message.includes('ROLE:') ||
message.includes('Generate all necessary files') ||
lowerMsg.includes('open ') ||
lowerMsg.includes('run ') ||
lowerMsg.includes('computer use')) {
finalMessage = systemContext + message;
const lowerMsg = message.toLowerCase();
if (message.includes('CREATE:') ||
message.includes('ROLE:') ||
message.includes('Generate all necessary files') ||
lowerMsg.includes('open ') ||
lowerMsg.includes('run ') ||
lowerMsg.includes('computer use')) {
finalMessage = systemContext + message;
}
}
return new Promise((resolve) => {
@@ -513,7 +519,7 @@ IMPORTANT RULES:
stream: false
};
const response = await fetch(QWEN_CHAT_API, {
const response = await fetchWithRetry(QWEN_CHAT_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -557,7 +563,7 @@ The Qwen Vision API needs OAuth authentication to analyze images. The current se
**To enable image analysis:**
1. Click "Authenticate Qwen" button to re-authenticate
2. Or describe what's in your image and I'll help without viewing it
*Your image was received (${(imageData?.length / 1024).toFixed(1)} KB) but couldn't be processed without Vision API tokens.*`,
usage: null
};

575
qwen-oauth.mjs.bak Normal file
View File

@@ -0,0 +1,575 @@
/**
* Qwen OAuth Implementation - Device Code Flow with PKCE
* Based on qwen-code's qwenOAuth2.ts
* https://github.com/QwenLM/qwen-code
*
* CONVERTED TO ESM for ink v5+ compatibility
*/
import crypto from 'crypto';
import fs from 'fs';
import { readFile, writeFile, unlink } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
// ESM __dirname equivalent
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Qwen OAuth Constants (from qwen-code)
const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`;
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
// Load config using createRequire (most reliable for cross-platform ESM/CJS compat)
let config = {};
try {
const require = createRequire(import.meta.url);
config = require('./config.cjs');
// Handle both ESM and CJS exports
if (config.default) config = config.default;
} catch (e) {
// Config missing is expected for first-time users using CLI only.
// We don't crash here - we just run without OAuth support (CLI fallback)
}
const QWEN_OAUTH_CLIENT_ID = config.QWEN_OAUTH_CLIENT_ID || null;
const QWEN_OAUTH_SCOPE = 'openid profile email model.completion';
const QWEN_OAUTH_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
const QWEN_CHAT_API = 'https://chat.qwen.ai/api/chat/completions';
// Token storage path
const TOKEN_FILE = path.join(__dirname, '.qwen-tokens.json');
/**
* Generate PKCE code verifier (RFC 7636)
*/
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
/**
* Generate PKCE code challenge from verifier
*/
function generateCodeChallenge(codeVerifier) {
const hash = crypto.createHash('sha256');
hash.update(codeVerifier);
return hash.digest('base64url');
}
/**
* Convert object to URL-encoded form data
*/
function objectToUrlEncoded(data) {
return Object.keys(data)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
.join('&');
}
/**
* Generate random UUID
*/
function randomUUID() {
return crypto.randomUUID();
}
class QwenOAuth {
constructor() {
this.tokens = null;
this.deviceCodeData = null;
this.codeVerifier = null;
}
/** Load stored tokens */
async loadTokens() {
try {
const data = await readFile(TOKEN_FILE, 'utf8');
this.tokens = JSON.parse(data);
return this.tokens;
} catch (error) {
this.tokens = null;
return null;
}
}
/** Save tokens */
async saveTokens(tokens) {
this.tokens = tokens;
// Add expiry timestamp
if (tokens.expires_in && !tokens.expiry_date) {
tokens.expiry_date = Date.now() + (tokens.expires_in * 1000);
}
await writeFile(TOKEN_FILE, JSON.stringify(tokens, null, 2));
}
/** Clear tokens */
async clearTokens() {
this.tokens = null;
this.deviceCodeData = null;
this.codeVerifier = null;
try {
await unlink(TOKEN_FILE);
} catch (error) { }
}
isTokenValid() {
if (!this.tokens || !this.tokens.access_token) {
return false;
}
if (this.tokens.expiry_date) {
// Check with 5 minute buffer
return Date.now() < (this.tokens.expiry_date - 300000);
}
return true;
}
async refreshToken() {
if (!this.tokens || !this.tokens.refresh_token) {
throw new Error('No refresh token available');
}
console.log('Refreshing access token...');
const bodyData = {
grant_type: 'refresh_token',
client_id: QWEN_OAUTH_CLIENT_ID,
refresh_token: this.tokens.refresh_token
};
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'x-request-id': randomUUID()
},
body: objectToUrlEncoded(bodyData)
});
if (!response.ok) {
const error = await response.text();
console.error('Token refresh failed:', response.status, error);
await this.clearTokens();
throw new Error(`Token refresh failed: ${response.status}`);
}
const newTokens = await response.json();
await this.saveTokens(newTokens);
console.log('Token refreshed successfully!');
return newTokens;
}
/**
* Start the Device Code Flow with PKCE
*/
async startDeviceFlow() {
console.log('Starting Qwen Device Code Flow with PKCE...');
if (!QWEN_OAUTH_CLIENT_ID) {
throw new Error('Missing Client ID. Please copy config.example.cjs to config.cjs and add your QWEN_OAUTH_CLIENT_ID to use this feature.');
}
// Generate PKCE pair
this.codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(this.codeVerifier);
const bodyData = {
client_id: QWEN_OAUTH_CLIENT_ID,
scope: QWEN_OAUTH_SCOPE,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
};
console.log('Device code request body:', bodyData);
const response = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'x-request-id': randomUUID()
},
body: objectToUrlEncoded(bodyData)
});
if (!response.ok) {
const error = await response.text();
console.error('Device code request failed:', response.status, error);
throw new Error(`Device code request failed: ${response.status} - ${error}`);
}
this.deviceCodeData = await response.json();
console.log('Device code response:', this.deviceCodeData);
// Check for error in response
if (this.deviceCodeData.error) {
throw new Error(`${this.deviceCodeData.error}: ${this.deviceCodeData.error_description || 'Unknown error'}`);
}
return {
verificationUri: this.deviceCodeData.verification_uri,
verificationUriComplete: this.deviceCodeData.verification_uri_complete,
userCode: this.deviceCodeData.user_code,
expiresIn: this.deviceCodeData.expires_in,
interval: this.deviceCodeData.interval || 5,
};
}
/**
* Poll for tokens after user completes login
*/
async pollForTokens() {
if (!this.deviceCodeData || !this.codeVerifier) {
throw new Error('Device flow not started');
}
const interval = (this.deviceCodeData.interval || 5) * 1000;
const endTime = Date.now() + (this.deviceCodeData.expires_in || 300) * 1000;
console.log(`Polling for tokens every ${interval / 1000}s...`);
while (Date.now() < endTime) {
try {
const bodyData = {
grant_type: QWEN_OAUTH_GRANT_TYPE,
device_code: this.deviceCodeData.device_code,
client_id: QWEN_OAUTH_CLIENT_ID,
code_verifier: this.codeVerifier
};
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'x-request-id': randomUUID()
},
body: objectToUrlEncoded(bodyData)
});
const data = await response.json();
if (response.ok && data.access_token) {
console.log('Token received successfully!');
await this.saveTokens(data);
this.deviceCodeData = null;
this.codeVerifier = null;
return data;
} else if (data.error === 'authorization_pending' || data.status === 'pending') {
// User hasn't completed auth yet
await new Promise(resolve => setTimeout(resolve, interval));
} else if (data.error === 'slow_down' || data.slowDown) {
// Slow down polling
await new Promise(resolve => setTimeout(resolve, interval * 2));
} else if (data.error === 'expired_token') {
throw new Error('Device code expired. Please start authentication again.');
} else if (data.error === 'access_denied') {
throw new Error('Access denied by user.');
} else if (data.error) {
throw new Error(`${data.error}: ${data.error_description || 'Unknown error'}`);
} else {
// Unknown response, keep polling
await new Promise(resolve => setTimeout(resolve, interval));
}
} catch (error) {
if (error.message.includes('expired') || error.message.includes('denied')) {
throw error;
}
console.error('Token poll error:', error.message);
await new Promise(resolve => setTimeout(resolve, interval));
}
}
throw new Error('Device flow timed out - please try again');
}
async getAccessToken() {
await this.loadTokens();
if (!this.tokens) {
throw new Error('Not authenticated. Please authenticate with Qwen first.');
}
if (!this.isTokenValid()) {
try {
await this.refreshToken();
} catch (error) {
throw new Error('Token expired. Please re-authenticate with Qwen.');
}
}
return this.tokens.access_token;
}
async checkAuth() {
const { exec } = await import('child_process');
// First check if we have OAuth tokens (needed for Vision API)
await this.loadTokens();
if (this.tokens && this.tokens.access_token) {
if (this.isTokenValid()) {
return { authenticated: true, method: 'oauth', hasVisionSupport: true };
} else {
// Try to refresh
try {
await this.refreshToken();
return { authenticated: true, method: 'oauth', hasVisionSupport: true };
} catch (e) {
// Token refresh failed, fall through to CLI check
}
}
}
// Fall back to CLI check (works for text but not Vision)
return new Promise((resolve) => {
exec('qwen -p "ping" --help 2>&1', { timeout: 5000 }, (error, stdout, stderr) => {
// If qwen CLI exists, consider it authenticated (it manages its own auth)
if (!error || stdout.includes('help') || stdout.includes('Usage')) {
resolve({ authenticated: true, method: 'qwen-cli', hasVisionSupport: false });
} else {
resolve({ authenticated: false, reason: 'qwen CLI not available or not authenticated' });
}
});
});
}
/** Send message using qwen CLI or Vision API for images
* @param {string} message - The message to send
* @param {string} model - The model to use
* @param {object} imageData - Optional image data
* @param {function} onChunk - Optional callback for streaming output (chunk) => void
*/
async sendMessage(message, model = 'qwen-coder-plus', imageData = null, onChunk = null) {
// If we have image data, always use the Vision API
if (imageData) {
console.log('📷 Image data detected, using Vision API...');
return await this.sendVisionMessage(message, imageData, 'qwen-vl-plus');
}
// For text-only, use the CLI
const { spawn } = await import('child_process');
const os = await import('os');
const fsSync = await import('fs');
// CRITICAL: Prepend system context to prevent AI from getting confused about environment
const systemContext = `[SYSTEM CONTEXT - ALWAYS FOLLOW]
You are an AI System Administrator integrated into OpenQode.
IMPORTANT RULES:
1. You have FULL ACCESS to the local file system.
2. You ARE PERMITTED to create folders, move files, and run shell commands.
3. When asked to create files, use the >>> CREATE: syntax.
4. When asked to manage files (move/delete), suggest the shell commands to do so.
5. Execute instructions immediately.
[END SYSTEM CONTEXT]
`;
// Prepend system context ONLY for build/create commands (detected by keywords)
let finalMessage = message;
const lowerMsg = message.toLowerCase();
if (message.includes('CREATE:') ||
message.includes('ROLE:') ||
message.includes('Generate all necessary files') ||
lowerMsg.includes('open ') ||
lowerMsg.includes('run ') ||
lowerMsg.includes('computer use')) {
finalMessage = systemContext + message;
}
return new Promise((resolve) => {
try {
// Debug log removed for clean UI
// Run in current project directory to allow context access
const neutralCwd = process.cwd();
// WINDOWS FIX: Execute JS directly to avoid cmd.exe argument splitting limits/bugs
// We derived this path from `where qwen` -> qwen.cmd -> cli.js location
const isWin = process.platform === 'win32';
let command = 'qwen';
let args = ['-p', finalMessage];
if (isWin) {
const appData = process.env.APPDATA || '';
const cliPath = path.join(appData, 'npm', 'node_modules', '@qwen-code', 'qwen-code', 'cli.js');
if (fs.existsSync(cliPath)) {
command = 'node';
args = [cliPath, '-p', finalMessage];
} else {
// Fallback if standard path fails (though known to exist on this machine)
command = 'qwen.cmd';
}
}
// Use spawn with shell: false (REQUIRED for clean argument passing)
const child = spawn(command, args, {
cwd: neutralCwd,
shell: false,
env: {
...process.env,
FORCE_COLOR: '0'
}
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
const chunk = data.toString();
stdout += chunk;
// Stream output in real-time if callback provided
if (onChunk) {
onChunk(chunk);
}
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
// Clean up ANSI codes
const cleanResponse = stdout.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '').trim();
// Debug log removed for clean UI
if (cleanResponse) {
resolve({
success: true,
response: cleanResponse,
usage: null
});
} else {
resolve({
success: false,
error: stderr || `CLI exited with code ${code}`,
response: ''
});
}
});
child.on('error', (error) => {
console.error('Qwen CLI spawn error:', error.message);
resolve({
success: false,
error: error.message || 'CLI execution failed',
response: ''
});
});
// Timeout after 120 seconds for long prompts
setTimeout(() => {
child.kill('SIGTERM');
resolve({
success: false,
error: 'Request timed out (120s)',
response: ''
});
}, 120000);
} catch (error) {
console.error('Qwen CLI error:', error.message);
resolve({
success: false,
error: error.message || 'CLI execution failed',
response: ''
});
}
});
}
/** Send message with image to Qwen Vision API */
async sendVisionMessage(message, imageData, model = 'qwen-vl-plus') {
try {
console.log('Sending vision message to Qwen VL API...');
// Get access token
const accessToken = await this.getAccessToken();
// Prepare the content array with image and text
const content = [];
// Add image (base64 data URL)
if (imageData) {
content.push({
type: 'image_url',
image_url: {
url: imageData // Already a data URL like "data:image/png;base64,..."
}
});
}
// Add text message
content.push({
type: 'text',
text: message
});
const requestBody = {
model: model,
messages: [
{
role: 'user',
content: content
}
],
stream: false
};
const response = await fetch(QWEN_CHAT_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'x-request-id': randomUUID()
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
console.error('Vision API error:', response.status, errorText);
return {
success: false,
error: `Vision API error: ${response.status}`,
response: ''
};
}
const data = await response.json();
const responseText = data.choices?.[0]?.message?.content || '';
console.log('Vision API response received:', responseText.substring(0, 100) + '...');
return {
success: true,
response: responseText,
usage: data.usage
};
} catch (error) {
console.error('Vision API error:', error.message);
// Provide helpful error message for auth issues
if (error.message.includes('authenticate') || error.message.includes('token')) {
return {
success: true, // Return as success with explanation
response: `⚠️ **Vision API Authentication Required**
The Qwen Vision API needs OAuth authentication to analyze images. The current session is authenticated for the CLI, but Vision API requires a separate OAuth token.
**To enable image analysis:**
1. Click "Authenticate Qwen" button to re-authenticate
2. Or describe what's in your image and I'll help without viewing it
*Your image was received (${(imageData?.length / 1024).toFixed(1)} KB) but couldn't be processed without Vision API tokens.*`,
usage: null
};
}
return {
success: false,
error: error.message || 'Vision API failed',
response: ''
};
}
}
}
export { QwenOAuth };