feat: core agent tools (fs, search, shell)
This commit is contained in:
96
src/App.tsx
96
src/App.tsx
@@ -1,49 +1,77 @@
|
||||
import { useState } from "react";
|
||||
import reactLogo from "./assets/react.svg";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { ToolTester } from "./components/ToolTester";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const [greetMsg, setGreetMsg] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [projectPath, setProjectPath] = useState<string | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
setGreetMsg(await invoke("greet", { name }));
|
||||
async function selectProject() {
|
||||
try {
|
||||
setErrorMsg(null);
|
||||
// Open native folder picker
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
if (selected === null) {
|
||||
// User cancelled selection
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke backend command to verify and set state
|
||||
// Note: invoke argument names must match Rust function args
|
||||
const confirmedPath = await invoke<string>("open_project", {
|
||||
path: selected,
|
||||
});
|
||||
setProjectPath(confirmedPath);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setErrorMsg(
|
||||
typeof e === "string" ? e : "An error occurred opening the project.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="container">
|
||||
<h1>Welcome to Tauri + React</h1>
|
||||
<h1>AI Code Assistant</h1>
|
||||
|
||||
<div className="row">
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://tauri.app" target="_blank">
|
||||
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
|
||||
{!projectPath ? (
|
||||
<div className="selection-screen">
|
||||
<p>
|
||||
Please select a project folder to start the Story-Driven Spec
|
||||
Workflow.
|
||||
</p>
|
||||
<button onClick={selectProject}>Open Project Directory</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="workspace">
|
||||
<div
|
||||
className="toolbar"
|
||||
style={{
|
||||
padding: "10px",
|
||||
background: "#f0f0f0",
|
||||
borderRadius: "4px",
|
||||
color: "#333",
|
||||
}}
|
||||
>
|
||||
<strong>Active Project:</strong> {projectPath}
|
||||
</div>
|
||||
<hr style={{ margin: "20px 0" }} />
|
||||
<p>Project loaded successfully.</p>
|
||||
<ToolTester />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
className="row"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id="greet-input"
|
||||
onChange={(e) => setName(e.currentTarget.value)}
|
||||
placeholder="Enter a name..."
|
||||
/>
|
||||
<button type="submit">Greet</button>
|
||||
</form>
|
||||
<p>{greetMsg}</p>
|
||||
{errorMsg && (
|
||||
<div className="error-message" style={{ marginTop: "20px" }}>
|
||||
<p style={{ color: "red" }}>Error: {errorMsg}</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
69
src/components/ToolTester.tsx
Normal file
69
src/components/ToolTester.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export function ToolTester() {
|
||||
const [output, setOutput] = useState<string>("Ready.");
|
||||
|
||||
const runCommand = async (name: string, args: Record<string, unknown>) => {
|
||||
setOutput(`Running ${name}...`);
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const res = await invoke(name, args);
|
||||
setOutput(JSON.stringify(res, null, 2));
|
||||
} catch (e) {
|
||||
setOutput(`Error: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ marginTop: "20px", border: "1px solid #ccc", padding: "10px" }}
|
||||
>
|
||||
<h3>Tool Tester</h3>
|
||||
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
|
||||
<button onClick={() => runCommand("list_directory", { path: "." })}>
|
||||
List Root
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
runCommand("read_file", { path: ".living_spec/README.md" })
|
||||
}
|
||||
>
|
||||
Read Spec
|
||||
</button>
|
||||
<button onClick={() => runCommand("search_files", { query: "Story" })}>
|
||||
Search "Story"
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
runCommand("exec_shell", { command: "ls", args: ["-F"] })
|
||||
}
|
||||
>
|
||||
Shell: ls -F
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
runCommand("exec_shell", { command: "git", args: ["status"] })
|
||||
}
|
||||
>
|
||||
Shell: git status
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<pre
|
||||
style={{
|
||||
marginTop: "10px",
|
||||
background: "#333",
|
||||
color: "#fff",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
overflowX: "auto",
|
||||
textAlign: "left",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
{output}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/types.ts
Normal file
15
src/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface FileEntry {
|
||||
name: string;
|
||||
kind: "file" | "dir";
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
path: string;
|
||||
matches: number;
|
||||
}
|
||||
|
||||
export interface CommandOutput {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exit_code: number;
|
||||
}
|
||||
Reference in New Issue
Block a user