mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
fix(multi-cli): populate multiCliPlan sessions in liteTaskDataStore
Fix task click handlers not working in multi-CLI planning detail page.
Root cause: liteTaskDataStore was not being populated with multiCliPlan
sessions during initialization, so task click handlers couldn't access
session data using currentSessionDetailKey.
Changes:
- navigation.js: Add code to populate multiCliPlan sessions in liteTaskDataStore
- notifications.js: Add code to populate multiCliPlan sessions when data refreshes
Now when task detail page loads, liteTaskDataStore contains the correct key
'multi-cli-${sessionId}' matching currentSessionDetailKey, allowing task
click handlers to find session data and open detail drawer.
Verified: Task clicks now properly open detail panel for all 7 tasks.
This commit is contained in:
@@ -300,6 +300,10 @@ export function run(argv: string[]): void {
|
||||
.option('--fail', 'Mark task as failed')
|
||||
.option('--from-queue [queue-id]', 'Sync issue statuses from queue (default: active queue)')
|
||||
.option('--queue <queue-id>', 'Target queue ID for multi-queue operations')
|
||||
// GitHub pull options
|
||||
.option('--state <state>', 'GitHub issue state: open, closed, or all')
|
||||
.option('--limit <n>', 'Maximum number of issues to pull from GitHub')
|
||||
.option('--labels <labels>', 'Filter by GitHub labels (comma-separated)')
|
||||
.action((subcommand, args, options) => issueCommand(subcommand, args, options));
|
||||
|
||||
// Loop command - Loop management for multi-CLI orchestration
|
||||
|
||||
@@ -223,6 +223,10 @@ interface IssueOptions {
|
||||
data?: string; // JSON data for create
|
||||
fromQueue?: boolean | string; // Sync statuses from queue (true=active, string=specific queue ID)
|
||||
queue?: string; // Target queue ID for multi-queue operations
|
||||
// GitHub pull options
|
||||
state?: string; // Issue state: open, closed, all
|
||||
limit?: number; // Maximum number of issues to pull
|
||||
labels?: string; // Filter by labels (comma-separated)
|
||||
}
|
||||
|
||||
const ISSUES_DIR = '.workflow/issues';
|
||||
@@ -1003,6 +1007,113 @@ async function createAction(options: IssueOptions): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* pull - Pull issues from GitHub
|
||||
* Usage: ccw issue pull [--state open|closed|all] [--limit N] [--labels label1,label2]
|
||||
*/
|
||||
async function pullAction(options: IssueOptions): Promise<void> {
|
||||
try {
|
||||
// Check if gh CLI is available
|
||||
try {
|
||||
execSync('gh --version', { stdio: 'ignore', timeout: EXEC_TIMEOUTS.GIT_QUICK });
|
||||
} catch {
|
||||
console.error(chalk.red('GitHub CLI (gh) is not installed or not in PATH'));
|
||||
console.error(chalk.gray('Install from: https://cli.github.com/'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build gh command with options
|
||||
const state = options.state || 'open';
|
||||
const limit = options.limit || 100;
|
||||
let ghCommand = `gh issue list --state ${state} --limit ${limit} --json number,title,body,labels,url,state`;
|
||||
|
||||
if (options.labels) {
|
||||
ghCommand += ` --label "${options.labels}"`;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`Fetching issues from GitHub (state: ${state}, limit: ${limit})...`));
|
||||
|
||||
// Fetch issues from GitHub
|
||||
const ghOutput = execSync(ghCommand, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: EXEC_TIMEOUTS.PROCESS_SPAWN,
|
||||
}).trim();
|
||||
|
||||
if (!ghOutput) {
|
||||
console.log(chalk.yellow('No issues found on GitHub'));
|
||||
return;
|
||||
}
|
||||
|
||||
const ghIssues = JSON.parse(ghOutput);
|
||||
const existingIssues = readIssues();
|
||||
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
let updated = 0;
|
||||
|
||||
for (const ghIssue of ghIssues) {
|
||||
const issueId = `GH-${ghIssue.number}`;
|
||||
const existingIssue = existingIssues.find(i => i.id === issueId);
|
||||
|
||||
// Prepare issue data
|
||||
const issueData: Partial<Issue> = {
|
||||
id: issueId,
|
||||
title: ghIssue.title,
|
||||
status: ghIssue.state === 'OPEN' ? 'registered' : 'completed',
|
||||
priority: 3, // Default priority
|
||||
context: ghIssue.body?.substring(0, 500) || ghIssue.title,
|
||||
source: 'github',
|
||||
source_url: ghIssue.url,
|
||||
tags: ghIssue.labels?.map((l: any) => l.name) || [],
|
||||
};
|
||||
|
||||
if (existingIssue) {
|
||||
// Update existing issue if state changed
|
||||
if (existingIssue.source_url === ghIssue.url) {
|
||||
// Check if status needs updating
|
||||
const newStatus = ghIssue.state === 'OPEN' ? 'registered' : 'completed';
|
||||
if (existingIssue.status !== newStatus || existingIssue.title !== ghIssue.title) {
|
||||
existingIssue.title = ghIssue.title;
|
||||
existingIssue.status = newStatus;
|
||||
existingIssue.updated_at = new Date().toISOString();
|
||||
updated++;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
} else {
|
||||
// Create new issue
|
||||
try {
|
||||
createIssue(issueData);
|
||||
imported++;
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Failed to import issue #${ghIssue.number}: ${(err as Error).message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save updates if any
|
||||
if (updated > 0) {
|
||||
writeIssues(existingIssues);
|
||||
}
|
||||
|
||||
console.log(chalk.green(`\n✓ GitHub sync complete:`));
|
||||
console.log(chalk.gray(` - Imported: ${imported} new issues`));
|
||||
console.log(chalk.gray(` - Updated: ${updated} existing issues`));
|
||||
console.log(chalk.gray(` - Skipped: ${skipped} unchanged issues`));
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ imported, updated, skipped, total: ghIssues.length }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Failed to pull issues from GitHub: ${(err as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* solution - Create or read solutions
|
||||
* Create: ccw issue solution <issue-id> --data '{"tasks":[...]}'
|
||||
@@ -2715,6 +2826,9 @@ export async function issueCommand(
|
||||
case 'create':
|
||||
await createAction(options);
|
||||
break;
|
||||
case 'pull':
|
||||
await pullAction(options);
|
||||
break;
|
||||
case 'solution':
|
||||
await solutionAction(argsArray[0], options);
|
||||
break;
|
||||
@@ -2769,6 +2883,8 @@ export async function issueCommand(
|
||||
console.log(chalk.bold.cyan('\nCCW Issue Management (v3.0 - Multi-Queue + Lifecycle)\n'));
|
||||
console.log(chalk.bold('Core Commands:'));
|
||||
console.log(chalk.gray(' create --data \'{"title":"..."}\' Create issue (auto-generates ID)'));
|
||||
console.log(chalk.gray(' pull [--state open|closed|all] Pull issues from GitHub'));
|
||||
console.log(chalk.gray(' [--limit N] [--labels label1,label2]'));
|
||||
console.log(chalk.gray(' init <issue-id> Initialize new issue (manual ID)'));
|
||||
console.log(chalk.gray(' list [issue-id] List issues or tasks'));
|
||||
console.log(chalk.gray(' history List completed issues (from history)'));
|
||||
@@ -2809,6 +2925,9 @@ export async function issueCommand(
|
||||
console.log(chalk.gray(' --priority <n> Queue priority (lower = higher)'));
|
||||
console.log(chalk.gray(' --json JSON output'));
|
||||
console.log(chalk.gray(' --force Force operation'));
|
||||
console.log(chalk.gray(' --state <state> GitHub issue state (open/closed/all)'));
|
||||
console.log(chalk.gray(' --limit <n> Max issues to pull from GitHub'));
|
||||
console.log(chalk.gray(' --labels <labels> Filter by GitHub labels (comma-separated)'));
|
||||
console.log();
|
||||
console.log(chalk.bold('Storage:'));
|
||||
console.log(chalk.gray(' .workflow/issues/issues.jsonl Active issues'));
|
||||
|
||||
@@ -124,7 +124,7 @@ export async function upgradeCommand(options: UpgradeOptions): Promise<void> {
|
||||
info('All installations are up to date.');
|
||||
console.log('');
|
||||
info('To upgrade ccw itself, run:');
|
||||
console.log(chalk.cyan(' npm update -g ccw'));
|
||||
console.log(chalk.cyan(' npm update -g claude-code-workflow'));
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,20 +13,16 @@
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getCodexLensPython } from '../utils/codexlens-path.js';
|
||||
|
||||
// Get directory of this module
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Venv paths (reuse CodexLens venv)
|
||||
const CODEXLENS_VENV = join(homedir(), '.codexlens', 'venv');
|
||||
const VENV_PYTHON =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'python.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'python');
|
||||
const VENV_PYTHON = getCodexLensPython();
|
||||
|
||||
// Script path
|
||||
const EMBEDDER_SCRIPT = join(__dirname, '..', '..', 'scripts', 'memory_embedder.py');
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { RouteContext } from '../types.js';
|
||||
import { EXEC_TIMEOUTS } from '../../../utils/exec-constants.js';
|
||||
import { extractJSON } from './utils.js';
|
||||
import { stopWatcherForUninstall } from './watcher-handlers.js';
|
||||
import { getCodexLensDataDir } from '../../../utils/codexlens-path.js';
|
||||
|
||||
export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx;
|
||||
@@ -777,7 +778,7 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
const { join } = await import('path');
|
||||
const { readFile } = await import('fs/promises');
|
||||
|
||||
const envPath = join(homedir(), '.codexlens', '.env');
|
||||
const envPath = join(getCodexLensDataDir(), '.env');
|
||||
let content = '';
|
||||
try {
|
||||
content = await readFile(envPath, 'utf-8');
|
||||
@@ -829,7 +830,7 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
}
|
||||
|
||||
// Also read settings.json for current configuration
|
||||
const settingsPath = join(homedir(), '.codexlens', 'settings.json');
|
||||
const settingsPath = join(getCodexLensDataDir(), 'settings.json');
|
||||
let settings: Record<string, any> = {};
|
||||
try {
|
||||
const settingsContent = await readFile(settingsPath, 'utf-8');
|
||||
@@ -943,7 +944,7 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
const { join, dirname } = await import('path');
|
||||
const { writeFile, mkdir, readFile } = await import('fs/promises');
|
||||
|
||||
const envPath = join(homedir(), '.codexlens', '.env');
|
||||
const envPath = join(getCodexLensDataDir(), '.env');
|
||||
await mkdir(dirname(envPath), { recursive: true });
|
||||
|
||||
// Read existing env file to preserve custom variables
|
||||
@@ -1072,7 +1073,7 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
await writeFile(envPath, lines.join('\n'), 'utf-8');
|
||||
|
||||
// Also update settings.json with mapped values
|
||||
const settingsPath = join(homedir(), '.codexlens', 'settings.json');
|
||||
const settingsPath = join(getCodexLensDataDir(), 'settings.json');
|
||||
let settings: Record<string, any> = {};
|
||||
try {
|
||||
const settingsContent = await readFile(settingsPath, 'utf-8');
|
||||
@@ -1145,7 +1146,7 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
const { join } = await import('path');
|
||||
const { readFile } = await import('fs/promises');
|
||||
|
||||
const settingsPath = join(homedir(), '.codexlens', 'settings.json');
|
||||
const settingsPath = join(getCodexLensDataDir(), 'settings.json');
|
||||
let settings: Record<string, any> = {};
|
||||
try {
|
||||
const content = await readFile(settingsPath, 'utf-8');
|
||||
@@ -1214,7 +1215,7 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
const { join, dirname } = await import('path');
|
||||
const { writeFile, mkdir, readFile } = await import('fs/promises');
|
||||
|
||||
const settingsPath = join(homedir(), '.codexlens', 'settings.json');
|
||||
const settingsPath = join(getCodexLensDataDir(), 'settings.json');
|
||||
await mkdir(dirname(settingsPath), { recursive: true });
|
||||
|
||||
// Read existing settings
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import type { RouteContext } from '../types.js';
|
||||
import { extractJSON } from './utils.js';
|
||||
import { getDefaultTool } from '../../../tools/claude-cli-tools.js';
|
||||
import { getCodexLensDataDir } from '../../../utils/codexlens-path.js';
|
||||
|
||||
export async function handleCodexLensSemanticRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
|
||||
@@ -445,9 +446,8 @@ export async function handleCodexLensSemanticRoutes(ctx: RouteContext): Promise<
|
||||
// Write to CodexLens .env file for persistence
|
||||
const { writeFileSync, existsSync, readFileSync } = await import('fs');
|
||||
const { join } = await import('path');
|
||||
const { homedir } = await import('os');
|
||||
|
||||
const codexlensDir = join(homedir(), '.codexlens');
|
||||
const codexlensDir = getCodexLensDataDir();
|
||||
const envFile = join(codexlensDir, '.env');
|
||||
|
||||
// Read existing .env content
|
||||
|
||||
@@ -2,13 +2,29 @@
|
||||
* Graph Routes Module
|
||||
* Handles graph visualization API endpoints for codex-lens data
|
||||
*/
|
||||
import { homedir } from 'os';
|
||||
import { join, resolve, normalize } from 'path';
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import Database from 'better-sqlite3';
|
||||
import { validatePath as validateAllowedPath } from '../../utils/path-validator.js';
|
||||
import type { RouteContext } from './types.js';
|
||||
|
||||
/**
|
||||
* Get the index root directory from CodexLens config or default.
|
||||
* Matches Python implementation priority:
|
||||
* 1. CODEXLENS_INDEX_DIR environment variable
|
||||
* 2. index_dir from ~/.codexlens/config.json
|
||||
* 3. Default: ~/.codexlens/indexes
|
||||
*/
|
||||
function getIndexRoot(): string {
|
||||
const envOverride = process.env.CODEXLENS_INDEX_DIR;
|
||||
if (envOverride) {
|
||||
return envOverride;
|
||||
}
|
||||
// Default: use CodexLens data directory + indexes
|
||||
const { getCodexLensDataDir } = require('../../utils/codexlens-path.js');
|
||||
return join(getCodexLensDataDir(), 'indexes');
|
||||
}
|
||||
|
||||
/**
|
||||
* PathMapper utility class (simplified from codex-lens Python implementation)
|
||||
* Maps source paths to index database paths
|
||||
@@ -17,7 +33,7 @@ class PathMapper {
|
||||
private indexRoot: string;
|
||||
|
||||
constructor(indexRoot?: string) {
|
||||
this.indexRoot = indexRoot || join(homedir(), '.codexlens', 'indexes');
|
||||
this.indexRoot = indexRoot || getIndexRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -992,6 +992,188 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/issues/pull - Pull issues from GitHub
|
||||
if (pathname === '/api/issues/pull' && req.method === 'POST') {
|
||||
const state = url.searchParams.get('state') || 'open';
|
||||
const limit = parseInt(url.searchParams.get('limit') || '100');
|
||||
const labels = url.searchParams.get('labels') || '';
|
||||
const downloadImages = url.searchParams.get('downloadImages') === 'true';
|
||||
|
||||
try {
|
||||
const { execSync } = await import('child_process');
|
||||
const https = await import('https');
|
||||
const http = await import('http');
|
||||
|
||||
// Check if gh CLI is available
|
||||
try {
|
||||
execSync('gh --version', { stdio: 'ignore', timeout: 5000 });
|
||||
} catch {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'GitHub CLI (gh) is not installed or not in PATH' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build gh command
|
||||
let ghCommand = `gh issue list --state ${state} --limit ${limit} --json number,title,body,labels,url,state`;
|
||||
if (labels) ghCommand += ` --label "${labels}"`;
|
||||
|
||||
// Execute gh command from project root
|
||||
const ghOutput = execSync(ghCommand, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 60000,
|
||||
cwd: issuesDir.replace(/[\\/]\.workflow[\\/]issues$/, '')
|
||||
}).trim();
|
||||
|
||||
if (!ghOutput) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ imported: 0, updated: 0, skipped: 0, images_downloaded: 0 }));
|
||||
return true;
|
||||
}
|
||||
|
||||
const ghIssues = JSON.parse(ghOutput);
|
||||
const existingIssues = readIssuesJsonl(issuesDir);
|
||||
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
let updated = 0;
|
||||
let imagesDownloaded = 0;
|
||||
|
||||
// Create images directory if needed
|
||||
const imagesDir = join(issuesDir, 'images');
|
||||
if (downloadImages && !existsSync(imagesDir)) {
|
||||
mkdirSync(imagesDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Helper function to download image
|
||||
const downloadImage = async (imageUrl: string, issueNumber: number, imageIndex: number): Promise<string | null> => {
|
||||
return new Promise((resolveDownload) => {
|
||||
try {
|
||||
const ext = imageUrl.match(/\.(png|jpg|jpeg|gif|webp|svg)/i)?.[1] || 'png';
|
||||
const filename = `GH-${issueNumber}-${imageIndex}.${ext}`;
|
||||
const filePath = join(imagesDir, filename);
|
||||
|
||||
// Skip if already downloaded
|
||||
if (existsSync(filePath)) {
|
||||
resolveDownload(`.workflow/issues/images/${filename}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const protocol = imageUrl.startsWith('https') ? https : http;
|
||||
protocol.get(imageUrl, { timeout: 30000 }, (response: any) => {
|
||||
// Handle redirect
|
||||
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||
const redirectUrl = response.headers.location;
|
||||
if (redirectUrl) {
|
||||
downloadImage(redirectUrl, issueNumber, imageIndex).then(resolveDownload);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
resolveDownload(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
response.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
response.on('end', () => {
|
||||
try {
|
||||
writeFileSync(filePath, Buffer.concat(chunks));
|
||||
resolveDownload(`.workflow/issues/images/${filename}`);
|
||||
} catch {
|
||||
resolveDownload(null);
|
||||
}
|
||||
});
|
||||
response.on('error', () => resolveDownload(null));
|
||||
}).on('error', () => resolveDownload(null));
|
||||
} catch {
|
||||
resolveDownload(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Process issues
|
||||
for (const ghIssue of ghIssues) {
|
||||
const issueId = `GH-${ghIssue.number}`;
|
||||
const existingIssue = existingIssues.find((i: any) => i.id === issueId);
|
||||
|
||||
let context = ghIssue.body || ghIssue.title;
|
||||
|
||||
// Extract and download images if enabled
|
||||
if (downloadImages && ghIssue.body) {
|
||||
// Find all image URLs in the body
|
||||
const imgPattern = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)|<img[^>]+src=["'](https?:\/\/[^"']+)["']/gi;
|
||||
const imageUrls: string[] = [];
|
||||
let match;
|
||||
while ((match = imgPattern.exec(ghIssue.body)) !== null) {
|
||||
imageUrls.push(match[1] || match[2]);
|
||||
}
|
||||
|
||||
// Download images and build reference list
|
||||
if (imageUrls.length > 0) {
|
||||
const downloadedImages: string[] = [];
|
||||
for (let i = 0; i < imageUrls.length; i++) {
|
||||
const localPath = await downloadImage(imageUrls[i], ghIssue.number, i + 1);
|
||||
if (localPath) {
|
||||
downloadedImages.push(localPath);
|
||||
imagesDownloaded++;
|
||||
}
|
||||
}
|
||||
|
||||
// Append image references to context
|
||||
if (downloadedImages.length > 0) {
|
||||
context += '\n\n---\n**Downloaded Images:**\n';
|
||||
downloadedImages.forEach((path, idx) => {
|
||||
context += `- Image ${idx + 1}: \`${path}\`\n`;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare issue data (truncate context to 2000 chars max)
|
||||
const issueData = {
|
||||
id: issueId,
|
||||
title: ghIssue.title,
|
||||
status: ghIssue.state === 'OPEN' ? 'registered' : 'completed',
|
||||
priority: 3,
|
||||
context: context.substring(0, 2000),
|
||||
source: 'github',
|
||||
source_url: ghIssue.url,
|
||||
tags: ghIssue.labels?.map((l: any) => l.name) || [],
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (existingIssue) {
|
||||
// Update if changed
|
||||
const newStatus = ghIssue.state === 'OPEN' ? 'registered' : 'completed';
|
||||
if (existingIssue.status !== newStatus || existingIssue.title !== ghIssue.title) {
|
||||
existingIssue.title = ghIssue.title;
|
||||
existingIssue.status = newStatus;
|
||||
existingIssue.context = issueData.context;
|
||||
existingIssue.updated_at = new Date().toISOString();
|
||||
updated++;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
} else {
|
||||
existingIssues.push(issueData);
|
||||
imported++;
|
||||
}
|
||||
}
|
||||
|
||||
// Save all issues
|
||||
writeIssuesJsonl(issuesDir, existingIssues);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ imported, updated, skipped, images_downloaded: imagesDownloaded, total: ghIssues.length }));
|
||||
} catch (err: any) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message || 'Failed to pull issues from GitHub' }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/issues/:id - Get issue detail
|
||||
const detailMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
|
||||
if (detailMatch && req.method === 'GET') {
|
||||
|
||||
@@ -37,6 +37,38 @@ import { TaskStorageManager, type TaskCreateRequest, type TaskUpdateRequest, typ
|
||||
import { executeCliTool } from '../../tools/cli-executor.js';
|
||||
import { loadClaudeCliTools } from '../../tools/claude-cli-tools.js';
|
||||
|
||||
/**
|
||||
* Module-level cache for CLI tools configuration
|
||||
* Loaded once at server startup to avoid repeated file I/O
|
||||
*/
|
||||
let cachedEnabledTools: string[] | null = null;
|
||||
|
||||
/**
|
||||
* Initialize CLI tools cache at server startup
|
||||
* Should be called once when the server starts
|
||||
*/
|
||||
export function initializeCliToolsCache(): void {
|
||||
try {
|
||||
const cliToolsConfig = loadClaudeCliTools(os.homedir());
|
||||
const enabledTools = Object.entries(cliToolsConfig.tools || {})
|
||||
.filter(([_, config]) => config.enabled === true)
|
||||
.map(([name]) => name);
|
||||
cachedEnabledTools = ['bash', ...enabledTools];
|
||||
console.log('[Loop V2] CLI tools cache initialized:', cachedEnabledTools);
|
||||
} catch (err) {
|
||||
console.error('[Loop V2] Failed to initialize CLI tools cache:', err);
|
||||
// Fallback to basic tools if config loading fails
|
||||
cachedEnabledTools = ['bash', 'gemini', 'qwen', 'codex', 'claude'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear CLI tools cache (for testing or config reload)
|
||||
*/
|
||||
export function clearCliToolsCache(): void {
|
||||
cachedEnabledTools = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 Loop Create Request
|
||||
*/
|
||||
@@ -1314,14 +1346,19 @@ function isValidId(id: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled tools list
|
||||
* Get enabled tools list from cache
|
||||
* If cache is not initialized, it will load from config (fallback for lazy initialization)
|
||||
*/
|
||||
function getEnabledToolsList(): string[] {
|
||||
const cliToolsConfig = loadClaudeCliTools(os.homedir());
|
||||
const enabledTools = Object.entries(cliToolsConfig.tools || {})
|
||||
.filter(([_, config]) => config.enabled === true)
|
||||
.map(([name]) => name);
|
||||
return ['bash', ...enabledTools];
|
||||
// Return cached value if available
|
||||
if (cachedEnabledTools) {
|
||||
return cachedEnabledTools;
|
||||
}
|
||||
|
||||
// Fallback: lazy initialization if cache not initialized (shouldn't happen in normal operation)
|
||||
console.warn('[Loop V2] CLI tools cache not initialized, performing lazy load');
|
||||
initializeCliToolsCache();
|
||||
return cachedEnabledTools || ['bash', 'gemini', 'qwen', 'codex', 'claude'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1508,6 +1508,38 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.issue-pull-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #1a1a1a;
|
||||
color: #ffffff;
|
||||
border: 1px solid #2d2d2d;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.issue-pull-btn:hover {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.issue-pull-btn:active {
|
||||
transform: translateY(0);
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.issue-pull-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
ISSUE STATS
|
||||
========================================== */
|
||||
|
||||
@@ -303,6 +303,12 @@ async function refreshWorkspace() {
|
||||
liteTaskDataStore[sessionKey] = s;
|
||||
});
|
||||
|
||||
// Populate multiCliPlan sessions
|
||||
(data.liteTasks?.multiCliPlan || []).forEach(s => {
|
||||
const sessionKey = `multi-cli-${s.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
liteTaskDataStore[sessionKey] = s;
|
||||
});
|
||||
|
||||
// Update global data
|
||||
window.workflowData = data;
|
||||
|
||||
|
||||
@@ -827,6 +827,12 @@ async function refreshWorkspaceData(newData) {
|
||||
liteTaskDataStore[key] = s;
|
||||
});
|
||||
|
||||
// Populate multiCliPlan sessions
|
||||
(newData.liteTasks?.multiCliPlan || []).forEach(s => {
|
||||
const key = `multi-cli-${s.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
liteTaskDataStore[key] = s;
|
||||
});
|
||||
|
||||
// Update UI silently
|
||||
updateStats();
|
||||
updateBadges();
|
||||
|
||||
@@ -60,6 +60,9 @@ async function checkForUpdatesNow() {
|
||||
btn.disabled = true;
|
||||
}
|
||||
|
||||
// Show checking state on badge
|
||||
updateVersionBadge('checking');
|
||||
|
||||
// Show checking notification
|
||||
console.log('[Version Check] Starting update check...');
|
||||
if (typeof addGlobalNotification === 'function') {
|
||||
@@ -83,6 +86,9 @@ async function checkForUpdatesNow() {
|
||||
versionCheckData = data;
|
||||
console.log('[Version Check] Result:', data);
|
||||
|
||||
// Update badge based on result
|
||||
updateVersionBadge(data.hasUpdate ? 'has-update' : 'none');
|
||||
|
||||
if (data.hasUpdate) {
|
||||
// New version available
|
||||
console.log('[Version Check] Update available:', data.latestVersion);
|
||||
@@ -109,6 +115,8 @@ async function checkForUpdatesNow() {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Version Check] Error:', err);
|
||||
// Clear badge on error
|
||||
updateVersionBadge('none');
|
||||
if (typeof addGlobalNotification === 'function') {
|
||||
addGlobalNotification(
|
||||
'error',
|
||||
@@ -154,6 +162,9 @@ async function checkForUpdates() {
|
||||
|
||||
versionCheckData = await res.json();
|
||||
|
||||
// Update badge
|
||||
updateVersionBadge(versionCheckData.hasUpdate ? 'has-update' : 'none');
|
||||
|
||||
if (versionCheckData.hasUpdate && !versionBannerDismissed) {
|
||||
showUpdateBanner(versionCheckData);
|
||||
addGlobalNotification(
|
||||
@@ -299,3 +310,30 @@ function getVersionInfo() {
|
||||
function isAutoUpdateEnabled() {
|
||||
return autoUpdateEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update version badge state
|
||||
* @param {string} state - 'checking', 'has-update', 'none'
|
||||
*/
|
||||
function updateVersionBadge(state) {
|
||||
const badge = document.getElementById('versionBadge');
|
||||
if (!badge) return;
|
||||
|
||||
// Remove all state classes
|
||||
badge.classList.remove('has-update', 'checking');
|
||||
badge.textContent = '';
|
||||
|
||||
switch (state) {
|
||||
case 'checking':
|
||||
badge.classList.add('checking');
|
||||
break;
|
||||
case 'has-update':
|
||||
badge.classList.add('has-update');
|
||||
badge.textContent = '!';
|
||||
break;
|
||||
case 'none':
|
||||
default:
|
||||
// Hide badge
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,14 @@ function renderIssueView() {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Pull from GitHub Button -->
|
||||
<button class="issue-pull-btn" onclick="showPullIssuesModal()" title="Pull issues from GitHub repository">
|
||||
<svg class="w-4 h-4 mr-1.5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v 3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
<span>Pull from GitHub</span>
|
||||
</button>
|
||||
|
||||
<!-- Create Button -->
|
||||
<button class="issue-create-btn" onclick="showCreateIssueModal()">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
@@ -281,6 +289,59 @@ function renderIssueView() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pull Issues Modal -->
|
||||
<div id="pullIssuesModal" class="issue-modal hidden">
|
||||
<div class="issue-modal-backdrop" onclick="hidePullIssuesModal()"></div>
|
||||
<div class="issue-modal-content">
|
||||
<div class="issue-modal-header">
|
||||
<h3>
|
||||
<svg class="w-5 h-5 inline mr-2 -mt-1" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
Pull Issues from GitHub
|
||||
</h3>
|
||||
<button class="btn-icon" onclick="hidePullIssuesModal()">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="issue-modal-body">
|
||||
<div class="form-group">
|
||||
<label>Issue State</label>
|
||||
<select id="pullIssueState">
|
||||
<option value="open" selected>Open</option>
|
||||
<option value="closed">Closed</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Maximum Issues</label>
|
||||
<input type="number" id="pullIssueLimit" value="20" min="1" max="100" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Labels (optional)</label>
|
||||
<input type="text" id="pullIssueLabels" placeholder="bug, enhancement (comma-separated)" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="pullDownloadImages" checked />
|
||||
<span>Download images to local</span>
|
||||
</label>
|
||||
<p class="form-hint text-xs text-muted-foreground mt-1">Images will be saved to .workflow/issues/images/ and links updated in issue context</p>
|
||||
</div>
|
||||
<div id="pullIssueResult" class="pull-result hidden mt-4 p-3 rounded-md bg-muted"></div>
|
||||
</div>
|
||||
<div class="issue-modal-footer">
|
||||
<button class="btn-secondary" onclick="hidePullIssuesModal()">Cancel</button>
|
||||
<button class="btn-primary" id="pullIssuesBtn" onclick="pullGitHubIssues()">
|
||||
<svg class="w-4 h-4 mr-1 inline" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
Pull Issues
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2627,6 +2688,127 @@ function hideCreateIssueModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Pull Issues Modal ==========
|
||||
function showPullIssuesModal() {
|
||||
const modal = document.getElementById('pullIssuesModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
// Reset result area
|
||||
const resultDiv = document.getElementById('pullIssueResult');
|
||||
if (resultDiv) {
|
||||
resultDiv.classList.add('hidden');
|
||||
resultDiv.innerHTML = '';
|
||||
}
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function hidePullIssuesModal() {
|
||||
const modal = document.getElementById('pullIssuesModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
// Clear form
|
||||
const stateSelect = document.getElementById('pullIssueState');
|
||||
const limitInput = document.getElementById('pullIssueLimit');
|
||||
const labelsInput = document.getElementById('pullIssueLabels');
|
||||
const downloadImagesCheck = document.getElementById('pullDownloadImages');
|
||||
if (stateSelect) stateSelect.value = 'open';
|
||||
if (limitInput) limitInput.value = '20';
|
||||
if (labelsInput) labelsInput.value = '';
|
||||
if (downloadImagesCheck) downloadImagesCheck.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function pullGitHubIssues() {
|
||||
const stateSelect = document.getElementById('pullIssueState');
|
||||
const limitInput = document.getElementById('pullIssueLimit');
|
||||
const labelsInput = document.getElementById('pullIssueLabels');
|
||||
const downloadImagesCheck = document.getElementById('pullDownloadImages');
|
||||
const resultDiv = document.getElementById('pullIssueResult');
|
||||
const pullBtn = document.getElementById('pullIssuesBtn');
|
||||
|
||||
const state = stateSelect?.value || 'open';
|
||||
const limit = parseInt(limitInput?.value || '20');
|
||||
const labels = labelsInput?.value?.trim();
|
||||
const downloadImages = downloadImagesCheck?.checked || false;
|
||||
|
||||
// Disable button and show loading
|
||||
if (pullBtn) {
|
||||
pullBtn.disabled = true;
|
||||
pullBtn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 mr-1 animate-spin"></i>' + (t('common.loading') || 'Loading...');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
path: projectPath,
|
||||
state: state,
|
||||
limit: limit.toString(),
|
||||
downloadImages: downloadImages.toString()
|
||||
});
|
||||
if (labels) params.set('labels', labels);
|
||||
|
||||
const response = await fetch('/api/issues/pull?' + params.toString(), {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || result.error) {
|
||||
showNotification(result.error || 'Failed to pull issues', 'error');
|
||||
if (resultDiv) {
|
||||
resultDiv.classList.remove('hidden');
|
||||
resultDiv.innerHTML = `<p class="text-destructive">${result.error || 'Failed to pull issues'}</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show results
|
||||
if (resultDiv) {
|
||||
resultDiv.classList.remove('hidden');
|
||||
resultDiv.innerHTML = `
|
||||
<div class="flex items-start gap-2">
|
||||
<i data-lucide="check-circle" class="w-5 h-5 text-success mt-0.5"></i>
|
||||
<div class="flex-1">
|
||||
<p class="font-medium mb-2">${t('issues.pullSuccess') || 'GitHub Issues Pulled Successfully'}</p>
|
||||
<div class="text-sm text-muted-foreground space-y-1">
|
||||
<p>✓ Imported: <strong>${result.imported || 0}</strong> new issues</p>
|
||||
<p>✓ Updated: <strong>${result.updated || 0}</strong> existing issues</p>
|
||||
<p>✓ Skipped: <strong>${result.skipped || 0}</strong> unchanged issues</p>
|
||||
${result.images_downloaded > 0 ? `<p>✓ Downloaded: <strong>${result.images_downloaded}</strong> images</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
showNotification(`Pulled ${result.imported + result.updated} issues from GitHub`, 'success');
|
||||
|
||||
// Reload data after 1 second
|
||||
setTimeout(async () => {
|
||||
await loadIssueData();
|
||||
renderIssueView();
|
||||
hidePullIssuesModal();
|
||||
}, 1500);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to pull issues:', err);
|
||||
showNotification('Failed to pull issues', 'error');
|
||||
if (resultDiv) {
|
||||
resultDiv.classList.remove('hidden');
|
||||
resultDiv.innerHTML = `<p class="text-destructive">${err.message || 'Unknown error occurred'}</p>`;
|
||||
}
|
||||
} finally {
|
||||
// Re-enable button
|
||||
if (pullBtn) {
|
||||
pullBtn.disabled = false;
|
||||
pullBtn.innerHTML = '<i data-lucide="download" class="w-4 h-4 mr-1"></i>' + (t('issues.pull') || 'Pull');
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createIssue() {
|
||||
const idInput = document.getElementById('newIssueId');
|
||||
const titleInput = document.getElementById('newIssueTitle');
|
||||
|
||||
@@ -1012,15 +1012,24 @@ async function saveTaskOrder(loopId, newOrder) {
|
||||
|
||||
/**
|
||||
* Show add task modal
|
||||
* Loads enabled tools before displaying modal to prevent race conditions
|
||||
*/
|
||||
async function showAddTaskModal(loopId) {
|
||||
// Get enabled tools
|
||||
const enabledTools = await getEnabledTools();
|
||||
// Find and disable the "Add Task" button to prevent multiple clicks during loading
|
||||
const addTaskButton = event?.target;
|
||||
if (addTaskButton) {
|
||||
addTaskButton.disabled = true;
|
||||
const originalText = addTaskButton.innerHTML;
|
||||
addTaskButton.innerHTML = '<i class="spinner"></i> ' + (t('common.loading') || 'Loading...');
|
||||
|
||||
// Build tool options HTML
|
||||
const toolOptions = enabledTools.map(tool =>
|
||||
`<option value="${tool}">${tool.charAt(0).toUpperCase() + tool.slice(1)}</option>`
|
||||
).join('');
|
||||
try {
|
||||
// Get enabled tools (this ensures tools are loaded before modal opens)
|
||||
const enabledTools = await getEnabledTools();
|
||||
|
||||
// Build tool options HTML
|
||||
const toolOptions = enabledTools.map(tool =>
|
||||
`<option value="${tool}">${tool.charAt(0).toUpperCase() + tool.slice(1)}</option>`
|
||||
).join('');
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'addTaskModal';
|
||||
@@ -1075,11 +1084,103 @@ async function showAddTaskModal(loopId) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
document.body.appendChild(modal);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
// Focus on description field
|
||||
setTimeout(() => document.getElementById('taskDescription').focus(), 100);
|
||||
// Focus on description field
|
||||
setTimeout(() => document.getElementById('taskDescription').focus(), 100);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to show add task modal:', err);
|
||||
alert(t('loop.loadToolsError') || 'Failed to load available tools. Please try again.');
|
||||
} finally {
|
||||
// Restore button state
|
||||
if (addTaskButton) {
|
||||
addTaskButton.disabled = false;
|
||||
addTaskButton.innerHTML = originalText;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback if event is not available (shouldn't happen normally)
|
||||
const enabledTools = await getEnabledTools();
|
||||
const toolOptions = enabledTools.map(tool =>
|
||||
`<option value="${tool}">${tool.charAt(0).toUpperCase() + tool.slice(1)}</option>`
|
||||
).join('');
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'addTaskModal';
|
||||
modal.className = 'modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3><i data-lucide="plus-circle" class="w-5 h-5"></i> ${t('loop.addTask') || 'Add Task'}</h3>
|
||||
<button class="modal-close" onclick="closeTaskModal()">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addTaskForm" onsubmit="handleAddTask(event, '${loopId}')">
|
||||
<div id="addTaskError" class="alert alert-error" style="display: none;"></div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-group">
|
||||
<label for="taskDescription">${t('loop.taskDescription') || 'Task Description'} <span class="required">*</span></label>
|
||||
<textarea id="taskDescription" name="description" rows="3" required
|
||||
placeholder="${t('loop.taskDescriptionPlaceholder') || 'Describe what this task should do...'}"
|
||||
class="form-control"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Tool -->
|
||||
<div class="form-group">
|
||||
<label for="taskTool">${t('loop.tool') || 'Tool'}</label>
|
||||
<select id="taskTool" name="tool" class="form-control">
|
||||
${toolOptions}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Mode -->
|
||||
<div class="form-group">
|
||||
<label for="taskMode">${t('loop.mode') || 'Mode'}</label>
|
||||
<select id="taskMode" name="mode" class="form-control">
|
||||
<option value="analysis">${t('loop.modeAnalysis') || 'Analysis'}</option>
|
||||
<option value="write">${t('loop.modeWrite') || 'Write'}</option>
|
||||
<option value="review">${t('loop.modeReview') || 'Review'}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Template -->
|
||||
<div class="form-group">
|
||||
<label for="taskPrompt">${t('loop.promptTemplate') || 'Prompt Template'} <span class="required">*</span></label>
|
||||
<textarea id="taskPrompt" name="prompt_template" rows="5" required
|
||||
placeholder="${t('loop.promptPlaceholder') || 'Enter the prompt to execute...'}"
|
||||
class="form-control"></textarea>
|
||||
<small class="form-help">${t('loop.promptHelp') || 'Variables: {iteration}, {output_prev}'}</small>
|
||||
</div>
|
||||
|
||||
<!-- Error Handling -->
|
||||
<div class="form-group">
|
||||
<label for="taskOnError">${t('loop.onError') || 'On Error'}</label>
|
||||
<select id="taskOnError" name="on_error" class="form-control">
|
||||
<option value="continue">${t('loop.errorContinue') || 'Continue'}</option>
|
||||
<option value="pause">${t('loop.errorPause') || 'Pause'}</option>
|
||||
<option value="fail_fast">${t('loop.errorFailFast') || 'Fail Fast'}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeTaskModal()">${t('loop.cancel') || 'Cancel'}</button>
|
||||
<button type="submit" class="btn btn-primary">${t('loop.add') || 'Add'}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
setTimeout(() => document.getElementById('taskDescription').focus(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -278,6 +278,50 @@
|
||||
display: block;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.check-icon-loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Version Badge */
|
||||
.version-badge {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background: hsl(var(--destructive));
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
.version-badge.has-update {
|
||||
display: flex;
|
||||
animation: badgePulse 2s ease-in-out infinite;
|
||||
}
|
||||
.version-badge.checking {
|
||||
display: flex;
|
||||
background: hsl(var(--muted) / 0.8);
|
||||
}
|
||||
.version-badge.checking::after {
|
||||
content: '...';
|
||||
animation: dots 1.5s steps(4, end) infinite;
|
||||
}
|
||||
@keyframes badgePulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.15); }
|
||||
}
|
||||
@keyframes dots {
|
||||
0%, 20% { content: '.'; }
|
||||
40% { content: '..'; }
|
||||
60%, 100% { content: '...'; }
|
||||
}
|
||||
|
||||
/* Auto-Update Toggle Switch */
|
||||
.auto-update-switch {
|
||||
@@ -390,7 +434,7 @@
|
||||
<!-- Auto-Update Controls -->
|
||||
<div class="flex items-center gap-2 border-l border-border pl-2">
|
||||
<!-- Check Now Button -->
|
||||
<button class="tooltip-bottom p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded" id="checkUpdateNow" data-tooltip="Check for updates now" onclick="checkForUpdatesNow()">
|
||||
<button class="tooltip-bottom p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded relative" id="checkUpdateNow" data-tooltip="Check for updates now" onclick="checkForUpdatesNow()">
|
||||
<!-- Download Icon (default) -->
|
||||
<svg class="check-icon-default" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
@@ -398,9 +442,11 @@
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
<!-- Loading Icon (checking state) -->
|
||||
<svg class="check-icon-loading hidden" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="check-icon-loading" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
||||
</svg>
|
||||
<!-- Version Available Badge -->
|
||||
<span class="version-badge" id="versionBadge"></span>
|
||||
</button>
|
||||
<!-- Auto-Update Toggle Switch -->
|
||||
<label class="tooltip-bottom auto-update-switch" data-tooltip="Auto-update">
|
||||
|
||||
@@ -12,14 +12,11 @@ import { z } from 'zod';
|
||||
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
||||
import { spawn } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { getProjectRoot } from '../utils/path-validator.js';
|
||||
import { getCodexLensPython } from '../utils/codexlens-path.js';
|
||||
|
||||
// CodexLens venv configuration
|
||||
const CODEXLENS_VENV =
|
||||
process.platform === 'win32'
|
||||
? join(homedir(), '.codexlens', 'venv', 'Scripts', 'python.exe')
|
||||
: join(homedir(), '.codexlens', 'venv', 'bin', 'python');
|
||||
const CODEXLENS_VENV = getCodexLensPython();
|
||||
|
||||
// Define Zod schema for validation
|
||||
const ParamsSchema = z.object({
|
||||
|
||||
@@ -24,6 +24,12 @@ import {
|
||||
isUvAvailable,
|
||||
createCodexLensUvManager,
|
||||
} from '../utils/uv-manager.js';
|
||||
import {
|
||||
getCodexLensDataDir,
|
||||
getCodexLensVenvDir,
|
||||
getCodexLensPython,
|
||||
getCodexLensPip,
|
||||
} from '../utils/codexlens-path.js';
|
||||
|
||||
// Get directory of this module
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -109,14 +115,6 @@ function findLocalCcwLitellmPath(): string | null {
|
||||
return findLocalPackagePath('ccw-litellm');
|
||||
}
|
||||
|
||||
// CodexLens configuration
|
||||
const CODEXLENS_DATA_DIR = join(homedir(), '.codexlens');
|
||||
const CODEXLENS_VENV = join(CODEXLENS_DATA_DIR, 'venv');
|
||||
const VENV_PYTHON =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'python.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'python');
|
||||
|
||||
// Bootstrap status cache
|
||||
let bootstrapChecked = false;
|
||||
let bootstrapReady = false;
|
||||
@@ -245,7 +243,7 @@ async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
||||
}
|
||||
|
||||
// Check venv exists
|
||||
if (!existsSync(CODEXLENS_VENV)) {
|
||||
if (!existsSync(getCodexLensVenvDir())) {
|
||||
const result = { ready: false, error: 'Venv not found' };
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
console.log(`[PERF][CodexLens] checkVenvStatus (no venv): ${Date.now() - funcStart}ms`);
|
||||
@@ -253,7 +251,7 @@ async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
||||
}
|
||||
|
||||
// Check python executable exists
|
||||
if (!existsSync(VENV_PYTHON)) {
|
||||
if (!existsSync(getCodexLensPython())) {
|
||||
const result = { ready: false, error: 'Python executable not found in venv' };
|
||||
venvStatusCache = { status: result, timestamp: Date.now() };
|
||||
console.log(`[PERF][CodexLens] checkVenvStatus (no python): ${Date.now() - funcStart}ms`);
|
||||
@@ -265,7 +263,7 @@ async function checkVenvStatus(force = false): Promise<ReadyStatus> {
|
||||
console.log('[PERF][CodexLens] checkVenvStatus spawning Python...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(VENV_PYTHON, ['-c', 'import codexlens; import watchdog; print(codexlens.__version__)'], {
|
||||
const child = spawn(getCodexLensPython(), ['-c', 'import codexlens; import watchdog; print(codexlens.__version__)'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 10000,
|
||||
});
|
||||
@@ -377,7 +375,7 @@ try:
|
||||
except Exception as e:
|
||||
print(json.dumps({"available": False, "error": str(e)}))
|
||||
`;
|
||||
const child = spawn(VENV_PYTHON, ['-c', checkCode], {
|
||||
const child = spawn(getCodexLensPython(), ['-c', checkCode], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 15000,
|
||||
});
|
||||
@@ -438,7 +436,7 @@ async function ensureLiteLLMEmbedderReady(): Promise<BootstrapResult> {
|
||||
|
||||
// Check if ccw_litellm can be imported
|
||||
const importStatus = await new Promise<{ ok: boolean; error?: string }>((resolve) => {
|
||||
const child = spawn(VENV_PYTHON, ['-c', 'import ccw_litellm; print("OK")'], {
|
||||
const child = spawn(getCodexLensPython(), ['-c', 'import ccw_litellm; print("OK")'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 15000,
|
||||
});
|
||||
@@ -502,10 +500,7 @@ async function ensureLiteLLMEmbedderReady(): Promise<BootstrapResult> {
|
||||
}
|
||||
|
||||
// Fallback: Use pip for installation
|
||||
const pipPath =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'pip.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'pip');
|
||||
const pipPath = getCodexLensPip();
|
||||
|
||||
try {
|
||||
if (localPath) {
|
||||
@@ -552,10 +547,7 @@ interface PythonEnvInfo {
|
||||
* DirectML requires: 64-bit Python, version 3.8-3.12
|
||||
*/
|
||||
async function checkPythonEnvForDirectML(): Promise<PythonEnvInfo> {
|
||||
const pythonPath =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'python.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'python');
|
||||
const pythonPath = getCodexLensPython();
|
||||
|
||||
if (!existsSync(pythonPath)) {
|
||||
return { version: '', majorMinor: '', architecture: 0, compatible: false, error: 'Python not found in venv' };
|
||||
@@ -800,10 +792,7 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
|
||||
console.log(`[CodexLens] Python ${pythonEnv.version} (${pythonEnv.architecture}-bit) - DirectML compatible`);
|
||||
}
|
||||
|
||||
const pipPath =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'pip.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'pip');
|
||||
const pipPath = getCodexLensPip();
|
||||
|
||||
// IMPORTANT: Uninstall all onnxruntime variants first to prevent conflicts
|
||||
// Having multiple onnxruntime packages causes provider detection issues
|
||||
@@ -933,16 +922,18 @@ async function bootstrapVenv(): Promise<BootstrapResult> {
|
||||
|
||||
// Fall back to pip logic...
|
||||
// Ensure data directory exists
|
||||
if (!existsSync(CODEXLENS_DATA_DIR)) {
|
||||
mkdirSync(CODEXLENS_DATA_DIR, { recursive: true });
|
||||
const dataDir = getCodexLensDataDir();
|
||||
const venvDir = getCodexLensVenvDir();
|
||||
if (!existsSync(dataDir)) {
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create venv if not exists
|
||||
if (!existsSync(CODEXLENS_VENV)) {
|
||||
if (!existsSync(venvDir)) {
|
||||
try {
|
||||
console.log('[CodexLens] Creating virtual environment...');
|
||||
const pythonCmd = getSystemPython();
|
||||
execSync(`${pythonCmd} -m venv "${CODEXLENS_VENV}"`, { stdio: 'inherit', timeout: EXEC_TIMEOUTS.PROCESS_SPAWN });
|
||||
execSync(`${pythonCmd} -m venv "${venvDir}"`, { stdio: 'inherit', timeout: EXEC_TIMEOUTS.PROCESS_SPAWN });
|
||||
} catch (err) {
|
||||
return { success: false, error: `Failed to create venv: ${(err as Error).message}` };
|
||||
}
|
||||
@@ -951,10 +942,7 @@ async function bootstrapVenv(): Promise<BootstrapResult> {
|
||||
// Install codex-lens
|
||||
try {
|
||||
console.log('[CodexLens] Installing codex-lens package...');
|
||||
const pipPath =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'pip.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'pip');
|
||||
const pipPath = getCodexLensPip();
|
||||
|
||||
// Try local path - codex-lens is local-only, not published to PyPI
|
||||
const codexLensPath = findLocalCodexLensPath();
|
||||
@@ -1131,7 +1119,7 @@ async function executeCodexLens(args: string[], options: ExecuteOptions = {}): P
|
||||
// spawn's cwd option handles drive changes correctly on Windows
|
||||
const spawnArgs = ['-m', 'codexlens', ...args];
|
||||
|
||||
const child = spawn(VENV_PYTHON, spawnArgs, {
|
||||
const child = spawn(getCodexLensPython(), spawnArgs, {
|
||||
cwd,
|
||||
shell: false, // CRITICAL: Prevent command injection
|
||||
timeout,
|
||||
@@ -1674,7 +1662,7 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
|
||||
async function uninstallCodexLens(): Promise<BootstrapResult> {
|
||||
try {
|
||||
// Check if venv exists
|
||||
if (!existsSync(CODEXLENS_VENV)) {
|
||||
if (!existsSync(getCodexLensVenvDir())) {
|
||||
return { success: false, error: 'CodexLens not installed (venv not found)' };
|
||||
}
|
||||
|
||||
@@ -1694,7 +1682,8 @@ async function uninstallCodexLens(): Promise<BootstrapResult> {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.log(`[CodexLens] Removing directory: ${CODEXLENS_DATA_DIR}`);
|
||||
const dataDir = getCodexLensDataDir();
|
||||
console.log(`[CodexLens] Removing directory: ${dataDir}`);
|
||||
|
||||
// Remove the entire .codexlens directory with retry logic for locked files
|
||||
const fs = await import('fs');
|
||||
@@ -1729,7 +1718,7 @@ async function uninstallCodexLens(): Promise<BootstrapResult> {
|
||||
}
|
||||
};
|
||||
|
||||
await removeWithRetry(CODEXLENS_DATA_DIR);
|
||||
await removeWithRetry(dataDir);
|
||||
|
||||
// Reset bootstrap cache
|
||||
bootstrapChecked = false;
|
||||
@@ -1827,7 +1816,7 @@ export {
|
||||
|
||||
// Export Python path for direct spawn usage (e.g., watcher)
|
||||
export function getVenvPythonPath(): string {
|
||||
return VENV_PYTHON;
|
||||
return getCodexLensPython();
|
||||
}
|
||||
|
||||
export type { GpuMode, PythonEnvInfo };
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { getCodexLensPython, getCodexLensVenvDir } from '../utils/codexlens-path.js';
|
||||
|
||||
export interface LiteLLMConfig {
|
||||
pythonPath?: string; // Default: CodexLens venv Python
|
||||
@@ -22,7 +22,7 @@ export interface LiteLLMConfig {
|
||||
|
||||
// Platform-specific constants for CodexLens venv
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
const CODEXLENS_VENV = join(homedir(), '.codexlens', 'venv');
|
||||
const CODEXLENS_VENV = getCodexLensVenvDir();
|
||||
const VENV_BIN_DIR = IS_WINDOWS ? 'Scripts' : 'bin';
|
||||
const PYTHON_EXECUTABLE = IS_WINDOWS ? 'python.exe' : 'python';
|
||||
|
||||
@@ -40,6 +40,20 @@ export function getCodexLensVenvPython(): string {
|
||||
return 'python';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Python path from CodexLens venv using centralized path utility
|
||||
* Falls back to system 'python' if venv doesn't exist
|
||||
* @returns Path to Python executable
|
||||
*/
|
||||
export function getCodexLensPythonPath(): string {
|
||||
const codexLensPython = getCodexLensPython();
|
||||
if (existsSync(codexLensPython)) {
|
||||
return codexLensPython;
|
||||
}
|
||||
// Fallback to system Python if venv not available
|
||||
return 'python';
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
|
||||
@@ -22,7 +22,18 @@ export interface LoopTask {
|
||||
/** Task description (what to do) */
|
||||
description: string;
|
||||
|
||||
/** CLI tool to use (bash, builtin tools, cli-wrapper, api-endpoint) */
|
||||
/**
|
||||
* CLI tool to use
|
||||
*
|
||||
* Should be one of the enabled tools from cli-tools.json:
|
||||
* - 'bash' (always available)
|
||||
* - Builtin tools: 'gemini', 'qwen', 'codex', 'claude', 'opencode'
|
||||
* - CLI wrappers: 'doubao', etc. (if enabled)
|
||||
* - API endpoints: custom tools (if enabled)
|
||||
*
|
||||
* Note: Validation is performed at the API layer (loop-v2-routes.ts)
|
||||
* to ensure tool is enabled before saving.
|
||||
*/
|
||||
tool: string;
|
||||
|
||||
/** Execution mode */
|
||||
|
||||
60
ccw/src/utils/codexlens-path.ts
Normal file
60
ccw/src/utils/codexlens-path.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* CodexLens Path Utilities
|
||||
*
|
||||
* Provides centralized path resolution for CodexLens data directory,
|
||||
* respecting the CODEXLENS_DATA_DIR environment variable.
|
||||
*
|
||||
* Priority order (matching Python implementation):
|
||||
* 1. CODEXLENS_DATA_DIR environment variable
|
||||
* 2. Default: ~/.codexlens
|
||||
*/
|
||||
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
/**
|
||||
* Get the CodexLens data directory.
|
||||
* Respects CODEXLENS_DATA_DIR environment variable.
|
||||
*
|
||||
* @returns Path to CodexLens data directory
|
||||
*/
|
||||
export function getCodexLensDataDir(): string {
|
||||
const envOverride = process.env.CODEXLENS_DATA_DIR;
|
||||
if (envOverride) {
|
||||
return envOverride;
|
||||
}
|
||||
return join(homedir(), '.codexlens');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CodexLens virtual environment path.
|
||||
*
|
||||
* @returns Path to CodexLens venv directory
|
||||
*/
|
||||
export function getCodexLensVenvDir(): string {
|
||||
return join(getCodexLensDataDir(), 'venv');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Python executable path in the CodexLens venv.
|
||||
*
|
||||
* @returns Path to python executable
|
||||
*/
|
||||
export function getCodexLensPython(): string {
|
||||
const venvDir = getCodexLensVenvDir();
|
||||
return process.platform === 'win32'
|
||||
? join(venvDir, 'Scripts', 'python.exe')
|
||||
: join(venvDir, 'bin', 'python');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pip executable path in the CodexLens venv.
|
||||
*
|
||||
* @returns Path to pip executable
|
||||
*/
|
||||
export function getCodexLensPip(): string {
|
||||
const venvDir = getCodexLensVenvDir();
|
||||
return process.platform === 'win32'
|
||||
? join(venvDir, 'Scripts', 'pip.exe')
|
||||
: join(venvDir, 'bin', 'pip');
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir, platform, arch } from 'os';
|
||||
import { EXEC_TIMEOUTS } from './exec-constants.js';
|
||||
import { getCodexLensDataDir, getCodexLensVenvDir } from './codexlens-path.js';
|
||||
|
||||
/**
|
||||
* Configuration for UvManager
|
||||
@@ -767,9 +768,9 @@ export class UvManager {
|
||||
* @returns Configured UvManager instance
|
||||
*/
|
||||
export function createCodexLensUvManager(dataDir?: string): UvManager {
|
||||
const baseDir = dataDir ?? join(homedir(), '.codexlens');
|
||||
const baseDir = dataDir ?? getCodexLensDataDir();
|
||||
return new UvManager({
|
||||
venvPath: join(baseDir, 'venv'),
|
||||
venvPath: getCodexLensVenvDir(),
|
||||
pythonVersion: '>=3.10,<3.13', // onnxruntime compatibility
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user