feat: enhance AI communication with dynamic system prompts, robust retry, and TUI formatters
This commit is contained in:
100
.agent/implementation_plan.md
Normal file
100
.agent/implementation_plan.md
Normal 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
|
||||
39
.opencode/implementation_plan.md
Normal file
39
.opencode/implementation_plan.md
Normal 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
20
.opencode/task.md
Normal 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
37
.opencode/walkthrough.md
Normal 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`
|
||||
110
Install.ps1
110
Install.ps1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||

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