Initial Release: OpenQode Public Alpha v1.3

This commit is contained in:
Gemini AI
2025-12-14 00:40:14 +04:00
Unverified
commit 8e8d80c110
119 changed files with 31174 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, useInput } from 'ink';
import fs from 'fs';
import path from 'path';
const h = React.createElement;
// Helper to sort: folders first
const sortFiles = (files, dirPath) => {
return files.sort((a, b) => {
const pathA = path.join(dirPath, a);
const pathB = path.join(dirPath, b);
try {
const statA = fs.statSync(pathA);
const statB = fs.statSync(pathB);
if (statA.isDirectory() && !statB.isDirectory()) return -1;
if (!statA.isDirectory() && statB.isDirectory()) return 1;
return a.localeCompare(b);
} catch (e) {
return 0;
}
});
};
const FileTree = ({
rootPath,
onSelect,
selectedFiles = new Set(),
isActive = false,
height = 20,
width = 30
}) => {
const [expanded, setExpanded] = useState(new Set([rootPath])); // Expanded folders
const [cursor, setCursor] = useState(rootPath); // Currently highlighted path
const [flatList, setFlatList] = useState([]); // Computed flat list for rendering (calc'd from expanded)
// Ignore list
const IGNORE_DIRS = new Set(['.git', 'node_modules', '.opencode', 'dist', 'build', 'coverage']);
// Rebuild flat list when expanded changes
// Returns array of { path, name, isDir, depth, isExpanded, hasChildren }
const buildFlatList = useCallback(() => {
const list = [];
const traverse = (currentPath, depth) => {
if (depth > 10) return; // Safety
const name = path.basename(currentPath) || (currentPath === rootPath ? '/' : currentPath);
let isDir = false;
try {
isDir = fs.statSync(currentPath).isDirectory();
} catch (e) { return; }
const isExpanded = expanded.has(currentPath);
list.push({
path: currentPath,
name: name,
isDir: isDir,
depth: depth,
isExpanded: isExpanded
});
if (isDir && isExpanded) {
try {
const children = fs.readdirSync(currentPath).filter(f => !IGNORE_DIRS.has(f) && !f.startsWith('.'));
const sorted = sortFiles(children, currentPath);
for (const child of sorted) {
traverse(path.join(currentPath, child), depth + 1);
}
} catch (e) {
// Permission error or file delete race condition
}
}
};
traverse(rootPath, 0);
return list;
}, [expanded, rootPath]);
useEffect(() => {
setFlatList(buildFlatList());
}, [buildFlatList]);
useInput((input, key) => {
if (!isActive) return;
const currentIndex = flatList.findIndex(item => item.path === cursor);
if (key.downArrow) {
const nextIndex = Math.min(flatList.length - 1, currentIndex + 1);
setCursor(flatList[nextIndex].path);
}
if (key.upArrow) {
const prevIndex = Math.max(0, currentIndex - 1);
setCursor(flatList[prevIndex].path);
}
if (key.rightArrow || key.return) {
const item = flatList[currentIndex];
if (item && item.isDir) {
if (!expanded.has(item.path)) {
setExpanded(prev => new Set([...prev, item.path]));
}
}
}
if (key.leftArrow) {
const item = flatList[currentIndex];
if (item && item.isDir && expanded.has(item.path)) {
const newExpanded = new Set(expanded);
newExpanded.delete(item.path);
setExpanded(newExpanded);
} else {
// Determine parent path to jump up
const parentPath = path.dirname(item.path);
if (parentPath && parentPath.length >= rootPath.length) {
setCursor(parentPath);
}
}
}
if (input === ' ') {
const item = flatList[currentIndex];
if (item && !item.isDir) {
// Toggle selection
if (onSelect) {
onSelect(item.path);
}
}
}
});
// Calculate viewport based on cursor
const cursorIndex = flatList.findIndex(item => item.path === cursor);
// Ensure height is valid number
const safeHeight = Math.max(5, height || 20);
const renderStart = Math.max(0, Math.min(cursorIndex - Math.floor(safeHeight / 2), flatList.length - safeHeight));
const renderEnd = Math.min(flatList.length, renderStart + safeHeight);
const visibleItems = flatList.slice(renderStart, renderEnd);
return h(Box, { flexDirection: 'column', width: width, height: safeHeight },
visibleItems.map((item) => {
const isSelected = selectedFiles.has(item.path);
const isCursor = item.path === cursor;
// Indentation
const indent = ' '.repeat(Math.max(0, item.depth));
// Icon
let icon = item.isDir
? (item.isExpanded ? '▼ ' : '▶ ')
: (isSelected ? '[x] ' : '[ ] ');
// Color logic
let color = 'white';
if (item.isDir) color = 'cyan';
if (isSelected) color = 'green';
// Cursor style
const bg = isCursor ? 'blue' : undefined;
const textColor = isCursor ? 'white' : color;
return h(Box, { key: item.path, width: '100%' },
h(Text, {
backgroundColor: bg,
color: textColor,
wrap: 'truncate'
}, `${indent}${icon}${item.name}`)
);
})
);
};
export default FileTree;