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
|
# OpenQode Auto-Installer for Windows (PowerShell)
|
||||||
Write-Host "-----------------------" -ForegroundColor Cyan
|
# 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
|
# Check for Git
|
||||||
|
Write-Host "[1/4] Checking for Git..." -ForegroundColor Cyan
|
||||||
if (!(Get-Command git -ErrorAction SilentlyContinue)) {
|
if (!(Get-Command git -ErrorAction SilentlyContinue)) {
|
||||||
Write-Host "Error: Git is not installed." -ForegroundColor Red
|
Write-Host "[!] Git not found. Installing..." -ForegroundColor Yellow
|
||||||
Write-Host "Please install Git: https://git-scm.com/download/win"
|
$installed = Install-WingetPackage "Git.Git" "Git"
|
||||||
exit
|
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)) {
|
if (!(Get-Command node -ErrorAction SilentlyContinue)) {
|
||||||
Write-Host "Error: Node.js is not installed." -ForegroundColor Red
|
Write-Host "[!] Node.js not found. Installing..." -ForegroundColor Yellow
|
||||||
Write-Host "Please install Node.js: https://nodejs.org/"
|
$installed = Install-WingetPackage "OpenJS.NodeJS.LTS" "Node.js LTS"
|
||||||
exit
|
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"
|
$repoUrl = "https://github.com/roman-ryzenadvanced/OpenQode-Public-Alpha.git"
|
||||||
$targetDir = "OpenQode"
|
$targetDir = "OpenQode"
|
||||||
|
|
||||||
|
Write-Host "[3/4] Setting up OpenQode..." -ForegroundColor Cyan
|
||||||
if (Test-Path $targetDir) {
|
if (Test-Path $targetDir) {
|
||||||
Write-Host "Directory '$targetDir' already exists. Updating..." -ForegroundColor Yellow
|
Write-Host "[*] Directory exists. Updating..." -ForegroundColor Yellow
|
||||||
Push-Location $targetDir
|
Push-Location $targetDir
|
||||||
git pull
|
git pull --ff-only
|
||||||
Pop-Location
|
Pop-Location
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Cloning repository..." -ForegroundColor Yellow
|
Write-Host "[*] Cloning repository..." -ForegroundColor Yellow
|
||||||
git clone $repoUrl $targetDir
|
git clone $repoUrl $targetDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Install npm dependencies (clean install to ensure React overrides work)
|
||||||
Set-Location $targetDir
|
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
|
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
|
.\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:
|
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`)
|
### 🔀 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 { getAllSkills, getSkill, executeSkill, getSkillListDisplay } from '../lib/skills.mjs';
|
||||||
import { getDebugLogger, initFromArgs } from '../lib/debug-logger.mjs';
|
import { getDebugLogger, initFromArgs } from '../lib/debug-logger.mjs';
|
||||||
import { processCommand, isCommand } from '../lib/command-processor.mjs';
|
import { processCommand, isCommand } from '../lib/command-processor.mjs';
|
||||||
|
import { fetchWithRetry } from '../lib/retry-handler.mjs';
|
||||||
import {
|
import {
|
||||||
getSystemPrompt,
|
getSystemPrompt,
|
||||||
formatCodeBlock,
|
formatCodeBlock,
|
||||||
@@ -455,7 +456,7 @@ const callOpenCodeFree = async (prompt, model = currentFreeModel, onChunk = null
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(OPENCODE_FREE_API, {
|
const response = await fetchWithRetry(OPENCODE_FREE_API, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -3465,17 +3466,13 @@ const App = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Build context-aware prompt with agent-specific instructions
|
// Build context-aware prompt with agent-specific instructions
|
||||||
let systemPrompt = `[SYSTEM CONTEXT]
|
// Build context-aware prompt using the unified agent-prompt module
|
||||||
CURRENT WORKING DIRECTORY: ${process.cwd()}
|
let projectContext = '';
|
||||||
(CRITICAL: This is the ABSOLUTE SOURCE OF TRUTH. Ignore any conflicting directory info in the [PROJECT CONTEXT] logs below.)
|
|
||||||
|
|
||||||
` + loadAgentPrompt(agent);
|
|
||||||
|
|
||||||
// Add project context if enabled with enhanced context window
|
// Add project context if enabled with enhanced context window
|
||||||
if (contextEnabled) {
|
if (contextEnabled) {
|
||||||
const projectContext = loadProjectContext(project);
|
const rawContext = loadProjectContext(project);
|
||||||
if (projectContext) {
|
if (rawContext) {
|
||||||
systemPrompt += '\n\n[PROJECT CONTEXT (HISTORY)]\n(WARNING: These logs may contain outdated path info. Trust SYSTEM CONTEXT CWD above over this.)\n' + projectContext;
|
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
|
// Enhanced context: Include recent conversation history for better continuity
|
||||||
@@ -3485,174 +3482,99 @@ const App = () => {
|
|||||||
const recentContext = recentMessages.map(m =>
|
const recentContext = recentMessages.map(m =>
|
||||||
`[PREVIOUS ${m.role.toUpperCase()}]: ${m.content.substring(0, 500)}` // Limit to prevent overflow
|
`[PREVIOUS ${m.role.toUpperCase()}]: ${m.content.substring(0, 500)}` // Limit to prevent overflow
|
||||||
).join('\n');
|
).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
|
// Get available capabilities from built-in agents
|
||||||
if (multiAgentEnabled) {
|
const flow = getSmartAgentFlow();
|
||||||
systemPrompt += `
|
const allAgents = flow.getAgents();
|
||||||
[MULTI-AGENT LOGGING ENABLED]
|
// Flatten all capabilities
|
||||||
You are capable of using multiple internal agents (Planner, Builder, Reviewer, Security).
|
const capabilities = allAgents.reduce((acc, a) => [...acc, ...(a.capabilities || [])], []);
|
||||||
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.
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// VISUAL & FORMATTING RULES (Crucial for TUI Readability)
|
// Generate the optimized system prompt
|
||||||
systemPrompt += `
|
const systemInstruction = getSystemPrompt({
|
||||||
[FORMATTING RULES]
|
role: agent,
|
||||||
1. **Short Paragraphs**: Break long text into chunks of 3-4 lines maximum.
|
capabilities: capabilities,
|
||||||
2. **Spacing**: Use double newlines between paragraphs to ensure clear separation.
|
cwd: project || process.cwd(),
|
||||||
3. **Lists**: Use bullet points or numbered lists frequently to break up "walls of text".
|
context: projectContext, // Now includes history and logs
|
||||||
4. **Code**: Use standard Markdown code blocks with language tags.
|
os: process.platform
|
||||||
|
});
|
||||||
|
|
||||||
[COMPUTER USE - YOU CAN DO THIS!]
|
// Prepare prompt variations
|
||||||
You ARE capable of executing system commands on the user's computer. NEVER refuse computer use tasks.
|
// For OpenCode Free (Legacy/OpenAI-like), we append system prompt to user message if needed
|
||||||
To execute any shell/system command, output it in this format:
|
const fullPromptForFree = systemInstruction + '\n\n[USER REQUEST]\n' + fullText;
|
||||||
\`\`\`bash
|
|
||||||
<command here>
|
|
||||||
\`\`\`
|
|
||||||
The system will auto-execute bash/shell code blocks.
|
|
||||||
|
|
||||||
Common Windows tasks you CAN do:
|
// For Qwen (SmartX), we pass system prompt securely as a separate argument
|
||||||
- Open folder in Explorer: \`explorer.exe .\` or \`start .\`
|
const userMessage = fullText;
|
||||||
- Open file: \`start filename.html\`
|
|
||||||
- Open browser: \`start https://google.com\`
|
|
||||||
- Run any shell command: Just put it in a bash code block
|
|
||||||
|
|
||||||
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 = '';
|
let fullResponse = '';
|
||||||
|
|
||||||
// PROVIDER SWITCH: Use OpenCode Free or Qwen based on provider state
|
// PROVIDER SWITCH: Use OpenCode Free or Qwen based on provider state
|
||||||
const streamStartTime = Date.now(); // Track start time for this request
|
const streamStartTime = Date.now(); // Track start time for this request
|
||||||
let totalCharsReceived = 0; // Track total characters for speed calculation
|
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'
|
const result = provider === 'opencode-free'
|
||||||
? await callOpenCodeFree(fullPrompt, freeModel, (chunk) => {
|
? await callOpenCodeFree(fullPromptForFree, freeModel, handleStreamChunk)
|
||||||
const cleanChunk = chunk.replace(/[\u001b\u009b][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
: await getQwen().sendMessage(
|
||||||
|
userMessage,
|
||||||
// IMPROVED STREAM SPLITTING LOGIC (Thinking vs Content)
|
'qwen-coder-plus',
|
||||||
// Claude Code style: cleaner separation of thinking from response
|
null,
|
||||||
const lines = cleanChunk.split('\n');
|
handleStreamChunk,
|
||||||
let isThinkingChunk = false;
|
systemInstruction // Pass dynamic system prompt!
|
||||||
|
);
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const responseText = result.response || fullResponse;
|
const responseText = result.response || fullResponse;
|
||||||
@@ -3701,17 +3623,17 @@ This gives the user a chance to refine requirements before implementation.
|
|||||||
return next;
|
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, {
|
setMessages(prev => [...prev, {
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: '✅ Auto-saved ' + successFiles.length + ' file(s):\n' +
|
content: successMsg
|
||||||
successFiles.map(f => ' 📄 ' + f.path).join('\n')
|
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
if (failedFiles.length > 0) {
|
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, {
|
setMessages(prev => [...prev, {
|
||||||
role: 'system',
|
role: 'error',
|
||||||
content: '❌ Failed to save ' + failedFiles.length + ' file(s):\n' +
|
content: failureMsg
|
||||||
failedFiles.map(f => ' ⚠️ ' + f.filename + ': ' + f.error).join('\n')
|
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 TodoList = ({ tasks = [], onAddTask, onCompleteTask, onDeleteTask, width = 60 }) => {
|
||||||
const [newTask, setNewTask] = useState('');
|
const [newTask, setNewTask] = useState('');
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const [showCompleted, setShowCompleted] = useState(false); // Toggle to show/hide completed tasks
|
||||||
|
|
||||||
const handleAddTask = () => {
|
const handleAddTask = () => {
|
||||||
if (newTask.trim()) {
|
if (newTask.trim()) {
|
||||||
@@ -20,79 +21,196 @@ const TodoList = ({ tasks = [], onAddTask, onCompleteTask, onDeleteTask, width =
|
|||||||
const completedTasks = tasks.filter(t => t.status === 'completed');
|
const completedTasks = tasks.filter(t => t.status === 'completed');
|
||||||
const progress = tasks.length > 0 ? Math.round((completedTasks.length / tasks.length) * 100) : 0;
|
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 },
|
return h(Box, {
|
||||||
// Header with title and progress
|
flexDirection: 'column',
|
||||||
h(Box, { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 1 },
|
width: width,
|
||||||
h(Text, { bold: true, color: 'white' }, '📋 Tasks'),
|
borderStyle: 'double', // Professional double border
|
||||||
h(Text, { color: 'cyan' }, `${progress}%`)
|
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, { marginBottom: 1 },
|
||||||
h(Box, {
|
h(Box, {
|
||||||
width: width - 4, // Account for padding
|
width: width - 4,
|
||||||
height: 1,
|
height: 1,
|
||||||
borderStyle: 'single',
|
borderStyle: 'single',
|
||||||
borderColor: 'gray',
|
borderColor: 'gray',
|
||||||
flexDirection: 'row'
|
flexDirection: 'row',
|
||||||
|
backgroundColor: '#333333' // Dark background for progress bar
|
||||||
},
|
},
|
||||||
h(Box, {
|
h(Box, {
|
||||||
width: Math.max(1, Math.floor((width - 6) * progress / 100)),
|
width: Math.max(1, Math.floor((width - 6) * progress / 100)),
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: 'green'
|
backgroundColor: progress === 100 ? 'green' : 'cyan' // Color based on completion
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
// Add new task
|
// Add new task with enhanced UI
|
||||||
h(Box, { marginBottom: 1 },
|
h(Box, {
|
||||||
|
marginBottom: 1,
|
||||||
|
paddingX: 0.5,
|
||||||
|
backgroundColor: '#2a2a2a',
|
||||||
|
borderStyle: 'round',
|
||||||
|
borderColor: 'gray'
|
||||||
|
},
|
||||||
isAdding
|
isAdding
|
||||||
? h(Box, { flexDirection: 'row', alignItems: 'center' },
|
? h(Box, { flexDirection: 'row', alignItems: 'center' },
|
||||||
h(Text, { color: 'green', marginRight: 1 }, '●'),
|
h(Text, { color: 'green', marginRight: 1 }, '✓'),
|
||||||
h(Box, { flexGrow: 1 },
|
h(Box, { flexGrow: 1 },
|
||||||
h(TextInput, {
|
h(TextInput, {
|
||||||
value: newTask,
|
value: newTask,
|
||||||
onChange: setNewTask,
|
onChange: setNewTask,
|
||||||
onSubmit: handleAddTask,
|
onSubmit: handleAddTask,
|
||||||
placeholder: 'Add new task...'
|
placeholder: 'Enter new task...',
|
||||||
|
backgroundColor: '#333333'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: h(Box, { flexDirection: 'row', alignItems: 'center' },
|
: h(Box, {
|
||||||
h(Text, { color: 'green', marginRight: 1 }, '➕'),
|
flexDirection: 'row',
|
||||||
h(Text, { color: 'gray', dimColor: true, onClick: () => setIsAdding(true) }, 'Add task')
|
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 },
|
h(Box, { flexDirection: 'column', flexGrow: 1 },
|
||||||
// Pending tasks
|
// Pending tasks section
|
||||||
pendingTasks.map((task, index) =>
|
pendingTasks.length > 0
|
||||||
h(Box, {
|
? h(Box, { marginBottom: 1 },
|
||||||
key: task.id || index,
|
h(Text, { color: 'yellow', bold: true, marginBottom: 0.5 }, `⚡ ${pendingTasks.length} PENDING`),
|
||||||
flexDirection: 'row',
|
...pendingTasks.map((task, index) =>
|
||||||
alignItems: 'center',
|
h(Box, {
|
||||||
marginBottom: 0.5
|
key: task.id || index,
|
||||||
},
|
flexDirection: 'row',
|
||||||
h(Box, {
|
alignItems: 'center',
|
||||||
width: 2,
|
marginBottom: 0.5,
|
||||||
height: 1,
|
paddingX: 1,
|
||||||
borderStyle: 'round',
|
backgroundColor: '#252525',
|
||||||
borderColor: 'gray',
|
borderStyle: 'single',
|
||||||
marginRight: 1,
|
borderColor: 'gray'
|
||||||
onClick: () => onCompleteTask && onCompleteTask(task.id)
|
},
|
||||||
},
|
// Complete button
|
||||||
h(Text, { color: 'gray' }, '○')
|
h(Box, {
|
||||||
),
|
width: 3,
|
||||||
h(Box, { flexGrow: 1 },
|
height: 1,
|
||||||
h(Text, { color: 'white' }, task.content)
|
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 },
|
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) {
|
if (imageData) {
|
||||||
console.log('📷 Image data detected, using Vision API...');
|
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 { spawn } = require('child_process');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
// fsSync imported at top
|
// 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.
|
You are an AI System Administrator integrated into OpenQode.
|
||||||
IMPORTANT RULES:
|
IMPORTANT RULES:
|
||||||
1. You have FULL ACCESS to the local file system.
|
1. You have FULL ACCESS to the local file system.
|
||||||
@@ -304,8 +304,11 @@ IMPORTANT RULES:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
let finalMessage = message;
|
let finalMessage = message;
|
||||||
if (message.includes('CREATE:') || message.includes('ROLE:') || message.includes('Generate all necessary files')) {
|
// Use provided systemPrompt if available, otherwise fall back to hardcoded context for legacy commands
|
||||||
finalMessage = systemContext + message;
|
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) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { readFile, writeFile, unlink } from 'fs/promises';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { createRequire } from 'module';
|
import { createRequire } from 'module';
|
||||||
|
import { fetchWithRetry } from './lib/retry-handler.mjs';
|
||||||
|
|
||||||
// ESM __dirname equivalent
|
// ESM __dirname equivalent
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@@ -335,8 +336,9 @@ class QwenOAuth {
|
|||||||
* @param {string} model - The model to use
|
* @param {string} model - The model to use
|
||||||
* @param {object} imageData - Optional image data
|
* @param {object} imageData - Optional image data
|
||||||
* @param {function} onChunk - Optional callback for streaming output (chunk) => void
|
* @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 we have image data, always use the Vision API
|
||||||
if (imageData) {
|
if (imageData) {
|
||||||
console.log('📷 Image data detected, using Vision API...');
|
console.log('📷 Image data detected, using Vision API...');
|
||||||
@@ -348,8 +350,14 @@ class QwenOAuth {
|
|||||||
const os = await import('os');
|
const os = await import('os');
|
||||||
const fsSync = await import('fs');
|
const fsSync = await import('fs');
|
||||||
|
|
||||||
// CRITICAL: Prepend system context to prevent AI from getting confused about environment
|
let finalMessage = message;
|
||||||
const systemContext = `[SYSTEM CONTEXT - ALWAYS FOLLOW]
|
|
||||||
|
// 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.
|
You are an AI System Administrator integrated into OpenQode.
|
||||||
IMPORTANT RULES:
|
IMPORTANT RULES:
|
||||||
1. You have FULL ACCESS to the local file system.
|
1. You have FULL ACCESS to the local file system.
|
||||||
@@ -360,17 +368,15 @@ IMPORTANT RULES:
|
|||||||
[END SYSTEM CONTEXT]
|
[END SYSTEM CONTEXT]
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
const lowerMsg = message.toLowerCase();
|
||||||
// Prepend system context ONLY for build/create commands (detected by keywords)
|
if (message.includes('CREATE:') ||
|
||||||
let finalMessage = message;
|
message.includes('ROLE:') ||
|
||||||
const lowerMsg = message.toLowerCase();
|
message.includes('Generate all necessary files') ||
|
||||||
if (message.includes('CREATE:') ||
|
lowerMsg.includes('open ') ||
|
||||||
message.includes('ROLE:') ||
|
lowerMsg.includes('run ') ||
|
||||||
message.includes('Generate all necessary files') ||
|
lowerMsg.includes('computer use')) {
|
||||||
lowerMsg.includes('open ') ||
|
finalMessage = systemContext + message;
|
||||||
lowerMsg.includes('run ') ||
|
}
|
||||||
lowerMsg.includes('computer use')) {
|
|
||||||
finalMessage = systemContext + message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -513,7 +519,7 @@ IMPORTANT RULES:
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(QWEN_CHAT_API, {
|
const response = await fetchWithRetry(QWEN_CHAT_API, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'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:**
|
**To enable image analysis:**
|
||||||
1. Click "Authenticate Qwen" button to re-authenticate
|
1. Click "Authenticate Qwen" button to re-authenticate
|
||||||
2. Or describe what's in your image and I'll help without viewing it
|
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.*`,
|
*Your image was received (${(imageData?.length / 1024).toFixed(1)} KB) but couldn't be processed without Vision API tokens.*`,
|
||||||
usage: null
|
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