mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat(workflow): add multi-CLI collaborative planning command
- Introduced a new command `/workflow:multi-cli-plan` for collaborative planning using ACE semantic search and iterative analysis with Claude and Codex. - Implemented a structured execution flow with phases for context gathering, multi-tool analysis, user decision points, and final plan generation. - Added detailed documentation outlining the command's usage, execution phases, and key features. - Included error handling and configuration options for enhanced user experience.
This commit is contained in:
@@ -416,5 +416,107 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise<boole
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: File dialog - list directory contents for file browser
|
||||
if (pathname === '/api/dialog/browse' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { path: browsePath, showHidden } = body as {
|
||||
path?: string;
|
||||
showHidden?: boolean;
|
||||
};
|
||||
|
||||
const os = await import('os');
|
||||
const path = await import('path');
|
||||
const fs = await import('fs');
|
||||
|
||||
// Default to home directory
|
||||
let targetPath = browsePath || os.homedir();
|
||||
|
||||
// Expand ~ to home directory
|
||||
if (targetPath.startsWith('~')) {
|
||||
targetPath = path.join(os.homedir(), targetPath.slice(1));
|
||||
}
|
||||
|
||||
// Resolve to absolute path
|
||||
if (!path.isAbsolute(targetPath)) {
|
||||
targetPath = path.resolve(targetPath);
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = await fs.promises.stat(targetPath);
|
||||
if (!stat.isDirectory()) {
|
||||
return { error: 'Path is not a directory', status: 400 };
|
||||
}
|
||||
|
||||
const entries = await fs.promises.readdir(targetPath, { withFileTypes: true });
|
||||
const items = entries
|
||||
.filter(entry => showHidden || !entry.name.startsWith('.'))
|
||||
.map(entry => ({
|
||||
name: entry.name,
|
||||
path: path.join(targetPath, entry.name),
|
||||
isDirectory: entry.isDirectory(),
|
||||
isFile: entry.isFile()
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
// Directories first, then files
|
||||
if (a.isDirectory && !b.isDirectory) return -1;
|
||||
if (!a.isDirectory && b.isDirectory) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return {
|
||||
currentPath: targetPath,
|
||||
parentPath: path.dirname(targetPath),
|
||||
items,
|
||||
homePath: os.homedir()
|
||||
};
|
||||
} catch (err) {
|
||||
return { error: 'Cannot access directory: ' + (err as Error).message, status: 400 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: File dialog - select file (validate path exists)
|
||||
if (pathname === '/api/dialog/open-file' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { path: filePath } = body as { path?: string };
|
||||
|
||||
if (!filePath) {
|
||||
return { error: 'Path is required', status: 400 };
|
||||
}
|
||||
|
||||
const os = await import('os');
|
||||
const path = await import('path');
|
||||
const fs = await import('fs');
|
||||
|
||||
let targetPath = filePath;
|
||||
|
||||
// Expand ~ to home directory
|
||||
if (targetPath.startsWith('~')) {
|
||||
targetPath = path.join(os.homedir(), targetPath.slice(1));
|
||||
}
|
||||
|
||||
// Resolve to absolute path
|
||||
if (!path.isAbsolute(targetPath)) {
|
||||
targetPath = path.resolve(targetPath);
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.access(targetPath, fs.constants.R_OK);
|
||||
const stat = await fs.promises.stat(targetPath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: targetPath,
|
||||
isFile: stat.isFile(),
|
||||
isDirectory: stat.isDirectory()
|
||||
};
|
||||
} catch {
|
||||
return { error: 'File not accessible', status: 404 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -661,3 +661,120 @@
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* File Browser Modal
|
||||
* ======================================== */
|
||||
|
||||
.file-browser-modal {
|
||||
width: 600px;
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.file-browser-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.file-browser-toolbar .btn-sm {
|
||||
flex-shrink: 0;
|
||||
padding: 0.375rem;
|
||||
}
|
||||
|
||||
.file-browser-path {
|
||||
flex: 1;
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.25rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.file-browser-hidden-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-browser-hidden-toggle input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-browser-list {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--background));
|
||||
}
|
||||
|
||||
.file-browser-loading,
|
||||
.file-browser-empty,
|
||||
.file-browser-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-browser-error {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.file-browser-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid hsl(var(--border) / 0.5);
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.file-browser-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-browser-item:hover {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
}
|
||||
|
||||
.file-browser-item.selected {
|
||||
background: hsl(var(--primary) / 0.15);
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.file-browser-item.is-directory {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.file-browser-item.is-file {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.file-browser-item-name {
|
||||
flex: 1;
|
||||
font-size: 0.8125rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -268,6 +268,12 @@ const i18n = {
|
||||
'cli.envFilePlaceholder': 'Path to .env file (e.g., ~/.gemini-env or C:/Users/xxx/.env)',
|
||||
'cli.envFileHint': 'Load environment variables (e.g., API keys) before CLI execution. Supports ~ for home directory.',
|
||||
'cli.envFileBrowse': 'Browse',
|
||||
'cli.fileBrowser': 'File Browser',
|
||||
'cli.fileBrowserSelect': 'Select',
|
||||
'cli.fileBrowserCancel': 'Cancel',
|
||||
'cli.fileBrowserUp': 'Parent Directory',
|
||||
'cli.fileBrowserHome': 'Home',
|
||||
'cli.fileBrowserShowHidden': 'Show hidden files',
|
||||
|
||||
// CodexLens Configuration
|
||||
'codexlens.config': 'CodexLens Configuration',
|
||||
@@ -2442,6 +2448,12 @@ const i18n = {
|
||||
'cli.envFilePlaceholder': '.env 文件路径(如 ~/.gemini-env 或 C:/Users/xxx/.env)',
|
||||
'cli.envFileHint': '在 CLI 执行前加载环境变量(如 API 密钥)。支持 ~ 表示用户目录。',
|
||||
'cli.envFileBrowse': '浏览',
|
||||
'cli.fileBrowser': '文件浏览器',
|
||||
'cli.fileBrowserSelect': '选择',
|
||||
'cli.fileBrowserCancel': '取消',
|
||||
'cli.fileBrowserUp': '上级目录',
|
||||
'cli.fileBrowserHome': '主目录',
|
||||
'cli.fileBrowserShowHidden': '显示隐藏文件',
|
||||
|
||||
// CodexLens 配置
|
||||
'codexlens.config': 'CodexLens 配置',
|
||||
|
||||
@@ -554,6 +554,241 @@ function buildToolConfigModalContent(tool, config, models, status) {
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// ========== File Browser Modal ==========
|
||||
|
||||
var fileBrowserState = {
|
||||
currentPath: '',
|
||||
showHidden: false,
|
||||
onSelect: null
|
||||
};
|
||||
|
||||
function showFileBrowserModal(onSelect) {
|
||||
fileBrowserState.onSelect = onSelect;
|
||||
fileBrowserState.showHidden = false;
|
||||
|
||||
// Create modal overlay
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'fileBrowserOverlay';
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.innerHTML = buildFileBrowserModalContent();
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Load initial directory (home)
|
||||
loadFileBrowserDirectory('');
|
||||
|
||||
// Initialize events
|
||||
initFileBrowserEvents();
|
||||
|
||||
// Initialize icons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function buildFileBrowserModalContent() {
|
||||
return '<div class="modal-content file-browser-modal">' +
|
||||
'<div class="modal-header">' +
|
||||
'<h3><i data-lucide="folder-open" class="w-4 h-4"></i> ' + t('cli.fileBrowser') + '</h3>' +
|
||||
'<button class="modal-close" id="fileBrowserCloseBtn">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="modal-body">' +
|
||||
'<div class="file-browser-toolbar">' +
|
||||
'<button class="btn-sm btn-outline" id="fileBrowserUpBtn" title="' + t('cli.fileBrowserUp') + '">' +
|
||||
'<i data-lucide="arrow-up" class="w-3.5 h-3.5"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn-sm btn-outline" id="fileBrowserHomeBtn" title="' + t('cli.fileBrowserHome') + '">' +
|
||||
'<i data-lucide="home" class="w-3.5 h-3.5"></i>' +
|
||||
'</button>' +
|
||||
'<input type="text" id="fileBrowserPathInput" class="file-browser-path" placeholder="/" readonly />' +
|
||||
'<label class="file-browser-hidden-toggle">' +
|
||||
'<input type="checkbox" id="fileBrowserShowHidden" />' +
|
||||
'<span>' + t('cli.fileBrowserShowHidden') + '</span>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="file-browser-list" id="fileBrowserList">' +
|
||||
'<div class="file-browser-loading"><i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="modal-footer">' +
|
||||
'<button class="btn btn-outline" id="fileBrowserCancelBtn">' + t('cli.fileBrowserCancel') + '</button>' +
|
||||
'<button class="btn btn-primary" id="fileBrowserSelectBtn" disabled>' +
|
||||
'<i data-lucide="check" class="w-3.5 h-3.5"></i> ' + t('cli.fileBrowserSelect') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
async function loadFileBrowserDirectory(path) {
|
||||
var listContainer = document.getElementById('fileBrowserList');
|
||||
var pathInput = document.getElementById('fileBrowserPathInput');
|
||||
|
||||
if (listContainer) {
|
||||
listContainer.innerHTML = '<div class="file-browser-loading"><i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i></div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/dialog/browse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: path, showHidden: fileBrowserState.showHidden })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load directory');
|
||||
}
|
||||
|
||||
var data = await response.json();
|
||||
fileBrowserState.currentPath = data.currentPath;
|
||||
|
||||
if (pathInput) {
|
||||
pathInput.value = data.currentPath;
|
||||
}
|
||||
|
||||
renderFileBrowserItems(data.items);
|
||||
} catch (err) {
|
||||
console.error('Failed to load directory:', err);
|
||||
if (listContainer) {
|
||||
listContainer.innerHTML = '<div class="file-browser-error">Failed to load directory</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderFileBrowserItems(items) {
|
||||
var listContainer = document.getElementById('fileBrowserList');
|
||||
if (!listContainer) return;
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
listContainer.innerHTML = '<div class="file-browser-empty">Empty directory</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = items.map(function(item) {
|
||||
var icon = item.isDirectory ? 'folder' : 'file';
|
||||
var itemClass = 'file-browser-item' + (item.isDirectory ? ' is-directory' : ' is-file');
|
||||
return '<div class="' + itemClass + '" data-path="' + escapeHtml(item.path) + '" data-is-dir="' + item.isDirectory + '">' +
|
||||
'<i data-lucide="' + icon + '" class="w-4 h-4"></i>' +
|
||||
'<span class="file-browser-item-name">' + escapeHtml(item.name) + '</span>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
|
||||
// Initialize icons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
// Add click handlers
|
||||
listContainer.querySelectorAll('.file-browser-item').forEach(function(el) {
|
||||
el.onclick = function() {
|
||||
var isDir = el.getAttribute('data-is-dir') === 'true';
|
||||
var path = el.getAttribute('data-path');
|
||||
|
||||
if (isDir) {
|
||||
// Navigate into directory
|
||||
loadFileBrowserDirectory(path);
|
||||
} else {
|
||||
// Select file
|
||||
listContainer.querySelectorAll('.file-browser-item').forEach(function(item) {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
el.classList.add('selected');
|
||||
|
||||
// Enable select button
|
||||
var selectBtn = document.getElementById('fileBrowserSelectBtn');
|
||||
if (selectBtn) {
|
||||
selectBtn.disabled = false;
|
||||
selectBtn.setAttribute('data-selected-path', path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Double-click to select file or enter directory
|
||||
el.ondblclick = function() {
|
||||
var isDir = el.getAttribute('data-is-dir') === 'true';
|
||||
var path = el.getAttribute('data-path');
|
||||
|
||||
if (isDir) {
|
||||
loadFileBrowserDirectory(path);
|
||||
} else {
|
||||
// Select and close
|
||||
closeFileBrowserModal(path);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function initFileBrowserEvents() {
|
||||
// Close button
|
||||
var closeBtn = document.getElementById('fileBrowserCloseBtn');
|
||||
if (closeBtn) {
|
||||
closeBtn.onclick = function() { closeFileBrowserModal(null); };
|
||||
}
|
||||
|
||||
// Cancel button
|
||||
var cancelBtn = document.getElementById('fileBrowserCancelBtn');
|
||||
if (cancelBtn) {
|
||||
cancelBtn.onclick = function() { closeFileBrowserModal(null); };
|
||||
}
|
||||
|
||||
// Select button
|
||||
var selectBtn = document.getElementById('fileBrowserSelectBtn');
|
||||
if (selectBtn) {
|
||||
selectBtn.onclick = function() {
|
||||
var path = selectBtn.getAttribute('data-selected-path');
|
||||
closeFileBrowserModal(path);
|
||||
};
|
||||
}
|
||||
|
||||
// Up button
|
||||
var upBtn = document.getElementById('fileBrowserUpBtn');
|
||||
if (upBtn) {
|
||||
upBtn.onclick = function() {
|
||||
// Get parent path
|
||||
var currentPath = fileBrowserState.currentPath;
|
||||
var parentPath = currentPath.replace(/[/\\][^/\\]+$/, '') || '/';
|
||||
loadFileBrowserDirectory(parentPath);
|
||||
};
|
||||
}
|
||||
|
||||
// Home button
|
||||
var homeBtn = document.getElementById('fileBrowserHomeBtn');
|
||||
if (homeBtn) {
|
||||
homeBtn.onclick = function() {
|
||||
loadFileBrowserDirectory('');
|
||||
};
|
||||
}
|
||||
|
||||
// Show hidden checkbox
|
||||
var showHiddenCheckbox = document.getElementById('fileBrowserShowHidden');
|
||||
if (showHiddenCheckbox) {
|
||||
showHiddenCheckbox.onchange = function() {
|
||||
fileBrowserState.showHidden = showHiddenCheckbox.checked;
|
||||
loadFileBrowserDirectory(fileBrowserState.currentPath);
|
||||
};
|
||||
}
|
||||
|
||||
// Click outside to close
|
||||
var overlay = document.getElementById('fileBrowserOverlay');
|
||||
if (overlay) {
|
||||
overlay.onclick = function(e) {
|
||||
if (e.target === overlay) {
|
||||
closeFileBrowserModal(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function closeFileBrowserModal(selectedPath) {
|
||||
var overlay = document.getElementById('fileBrowserOverlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
|
||||
if (fileBrowserState.onSelect && selectedPath) {
|
||||
fileBrowserState.onSelect(selectedPath);
|
||||
}
|
||||
|
||||
fileBrowserState.onSelect = null;
|
||||
}
|
||||
|
||||
function initToolConfigModalEvents(tool, currentConfig, models) {
|
||||
// Local tags state (copy from config)
|
||||
var currentTags = (currentConfig.tags || []).slice();
|
||||
@@ -754,38 +989,13 @@ function initToolConfigModalEvents(tool, currentConfig, models) {
|
||||
// Environment file browse button (only for gemini/qwen)
|
||||
var envFileBrowseBtn = document.getElementById('envFileBrowseBtn');
|
||||
if (envFileBrowseBtn) {
|
||||
envFileBrowseBtn.onclick = async function() {
|
||||
try {
|
||||
// Use file dialog API if available
|
||||
var response = await fetch('/api/dialog/open-file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: t('cli.envFile'),
|
||||
filters: [
|
||||
{ name: 'Environment Files', extensions: ['env'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
],
|
||||
defaultPath: ''
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
var data = await response.json();
|
||||
if (data.filePath) {
|
||||
var envFileInput = document.getElementById('envFileInput');
|
||||
if (envFileInput) {
|
||||
envFileInput.value = data.filePath;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: prompt user to enter path manually
|
||||
showRefreshToast('File dialog not available. Please enter path manually.', 'info');
|
||||
envFileBrowseBtn.onclick = function() {
|
||||
showFileBrowserModal(function(selectedPath) {
|
||||
var envFileInput = document.getElementById('envFileInput');
|
||||
if (envFileInput && selectedPath) {
|
||||
envFileInput.value = selectedPath;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to open file dialog:', err);
|
||||
showRefreshToast('File dialog not available. Please enter path manually.', 'info');
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user