2026-02-13 12:31:36 +00:00
|
|
|
import * as React from "react";
|
|
|
|
|
import { api } from "./api/client";
|
|
|
|
|
import { Chat } from "./components/Chat";
|
2026-02-16 19:59:37 +00:00
|
|
|
import { SelectionScreen } from "./components/selection/SelectionScreen";
|
|
|
|
|
import { usePathCompletion } from "./components/selection/usePathCompletion";
|
2026-02-13 12:31:36 +00:00
|
|
|
import "./App.css";
|
|
|
|
|
|
|
|
|
|
function App() {
|
2026-02-16 20:34:03 +00:00
|
|
|
const [projectPath, setProjectPath] = React.useState<string | null>(null);
|
|
|
|
|
const [errorMsg, setErrorMsg] = React.useState<string | null>(null);
|
|
|
|
|
const [pathInput, setPathInput] = React.useState("");
|
|
|
|
|
const [isOpening, setIsOpening] = React.useState(false);
|
|
|
|
|
const [knownProjects, setKnownProjects] = React.useState<string[]>([]);
|
|
|
|
|
const [homeDir, setHomeDir] = React.useState<string | null>(null);
|
2026-02-16 19:44:29 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
api
|
|
|
|
|
.getKnownProjects()
|
|
|
|
|
.then((projects) => setKnownProjects(projects))
|
|
|
|
|
.catch((error) => console.error(error));
|
|
|
|
|
}, []);
|
2026-02-13 12:31:36 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
let active = true;
|
|
|
|
|
api
|
|
|
|
|
.getHomeDirectory()
|
|
|
|
|
.then((home) => {
|
|
|
|
|
if (!active) return;
|
|
|
|
|
setHomeDir(home);
|
|
|
|
|
setPathInput((current) => {
|
|
|
|
|
if (current.trim()) {
|
|
|
|
|
return current;
|
|
|
|
|
}
|
|
|
|
|
const initial = home.endsWith("/") ? home : `${home}/`;
|
|
|
|
|
return initial;
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error(error);
|
|
|
|
|
});
|
2026-02-16 19:44:29 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
return () => {
|
|
|
|
|
active = false;
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
2026-02-16 19:44:29 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
const {
|
|
|
|
|
matchList,
|
|
|
|
|
selectedMatch,
|
|
|
|
|
suggestionTail,
|
|
|
|
|
completionError,
|
|
|
|
|
currentPartial,
|
|
|
|
|
setSelectedMatch,
|
|
|
|
|
acceptSelectedMatch,
|
|
|
|
|
acceptMatch,
|
|
|
|
|
closeSuggestions,
|
|
|
|
|
} = usePathCompletion({
|
|
|
|
|
pathInput,
|
|
|
|
|
setPathInput,
|
|
|
|
|
homeDir,
|
|
|
|
|
listDirectoryAbsolute: api.listDirectoryAbsolute,
|
|
|
|
|
});
|
2026-02-16 19:44:29 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
async function openProject(path: string) {
|
|
|
|
|
const trimmedPath = path.trim();
|
|
|
|
|
if (!trimmedPath) {
|
|
|
|
|
setErrorMsg("Please enter a project path.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-13 12:31:36 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
try {
|
|
|
|
|
setErrorMsg(null);
|
|
|
|
|
setIsOpening(true);
|
|
|
|
|
const confirmedPath = await api.openProject(trimmedPath);
|
|
|
|
|
setProjectPath(confirmedPath);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
const message =
|
|
|
|
|
e instanceof Error
|
|
|
|
|
? e.message
|
|
|
|
|
: typeof e === "string"
|
|
|
|
|
? e
|
|
|
|
|
: "An error occurred opening the project.";
|
2026-02-13 12:31:36 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
setErrorMsg(message);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsOpening(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-13 12:31:36 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
function handleOpen() {
|
|
|
|
|
void openProject(pathInput);
|
|
|
|
|
}
|
2026-02-16 19:59:37 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
async function handleForgetProject(path: string) {
|
|
|
|
|
try {
|
|
|
|
|
await api.forgetKnownProject(path);
|
|
|
|
|
setKnownProjects((prev) => prev.filter((p) => p !== path));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-13 12:31:36 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
async function closeProject() {
|
|
|
|
|
try {
|
|
|
|
|
await api.closeProject();
|
|
|
|
|
setProjectPath(null);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-16 19:48:39 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
function handlePathInputKeyDown(
|
|
|
|
|
event: React.KeyboardEvent<HTMLInputElement>,
|
|
|
|
|
) {
|
|
|
|
|
if (event.key === "ArrowDown") {
|
|
|
|
|
if (matchList.length > 0) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
setSelectedMatch((selectedMatch + 1) % matchList.length);
|
|
|
|
|
}
|
|
|
|
|
} else if (event.key === "ArrowUp") {
|
|
|
|
|
if (matchList.length > 0) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
setSelectedMatch(
|
|
|
|
|
(selectedMatch - 1 + matchList.length) % matchList.length,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else if (event.key === "Tab") {
|
|
|
|
|
if (matchList.length > 0) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
acceptSelectedMatch();
|
|
|
|
|
}
|
|
|
|
|
} else if (event.key === "Escape") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
closeSuggestions();
|
|
|
|
|
} else if (event.key === "Enter") {
|
|
|
|
|
handleOpen();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-16 18:57:39 +00:00
|
|
|
|
2026-02-16 20:34:03 +00:00
|
|
|
return (
|
|
|
|
|
<main
|
|
|
|
|
className="container"
|
|
|
|
|
style={{ height: "100vh", padding: 0, maxWidth: "100%" }}
|
|
|
|
|
>
|
|
|
|
|
{!projectPath ? (
|
|
|
|
|
<SelectionScreen
|
|
|
|
|
knownProjects={knownProjects}
|
|
|
|
|
onOpenProject={openProject}
|
|
|
|
|
onForgetProject={handleForgetProject}
|
|
|
|
|
pathInput={pathInput}
|
|
|
|
|
homeDir={homeDir}
|
|
|
|
|
onPathInputChange={setPathInput}
|
|
|
|
|
onPathInputKeyDown={handlePathInputKeyDown}
|
|
|
|
|
isOpening={isOpening}
|
|
|
|
|
suggestionTail={suggestionTail}
|
|
|
|
|
matchList={matchList}
|
|
|
|
|
selectedMatch={selectedMatch}
|
|
|
|
|
onSelectMatch={setSelectedMatch}
|
|
|
|
|
onAcceptMatch={acceptMatch}
|
|
|
|
|
onCloseSuggestions={closeSuggestions}
|
|
|
|
|
completionError={completionError}
|
|
|
|
|
currentPartial={currentPartial}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="workspace" style={{ height: "100%" }}>
|
|
|
|
|
<Chat projectPath={projectPath} onCloseProject={closeProject} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{errorMsg && (
|
|
|
|
|
<div className="error-message" style={{ marginTop: "20px" }}>
|
|
|
|
|
<p style={{ color: "red" }}>Error: {errorMsg}</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
);
|
2026-02-13 12:31:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default App;
|