History

feat: API contract, server handler, and frontend with effect-atom

- packages/core/api: HttpApi contract with ToolsApi + SecretsApi groups SDK tagged errors pass through directly with status annotations (ToolNotFoundError→404, PolicyDeniedError→403, etc.) - apps/server: API handler implementation via HttpApiBuilder Exports createApiHandler() for reuse (no baked-in server runtime) Handlers are pure pass-through — no manual error catching - apps/web: Vite + React frontend served on same port as API Vite plugin forwards /v1/*, /docs, /openapi.json to Effect handler effect-atom integration: AtomHttpApi.Tag typed client, query/mutation atoms RegistryProvider + useAtomValue with Result pattern matching - Catalog pins @effect/platform, platform-bun, platform-node, cluster - All 9 packages typecheck clean

Rhys Sullivan committed 7 months ago

Commit 8babea81 parent 00219a926 files · +1022 −32Browse files

Showing the changes to apps/web/src (5 of 26 files). Show all

Unified · Split

5 files
  1. apps/web/src/App.tsx +46 −0
  2. apps/web/src/api/atoms.ts +40 −0
  3. apps/web/src/api/client.ts +15 −0
  4. apps/web/src/api/index.ts +10 −0
  5. apps/web/src/main.tsx +6 −0
apps/web/src/App.tsx+46−0
@@ -0,0 +1,46 @@
1import React, { Suspense } from "react";
2import { RegistryProvider, useAtomValue, Result } from "@effect-atom/atom-react";
3
4import { ExecutorClient } from "./api/client";
5import { toolsAtom } from "./api/atoms";
6
7function ToolList() {
8 const tools = useAtomValue(toolsAtom());
9
10 if (tools._tag === "Initial" || tools.waiting) {
11 return <p>Loading tools…</p>;
12 }
13
14 if (tools._tag === "Failure") {
15 return <p style={{ color: "red" }}>Failed to load tools</p>;
16 }
17
18 return (
19 <div>
20 <h2>Tools ({tools.value.length})</h2>
21 {tools.value.length === 0 ? (
22 <p style={{ color: "#888" }}>No tools registered yet.</p>
23 ) : (
24 <ul>
25 {tools.value.map((t) => (
26 <li key={t.id}>
27 <strong>{t.name}</strong>
28 {t.description && <span> — {t.description}</span>}
29 </li>
30 ))}
31 </ul>
32 )}
33 </div>
34 );
35}
36
37export function App() {
38 return (
39 <RegistryProvider>
40 <div style={{ fontFamily: "system-ui", padding: "2rem" }}>
41 <h1>Executor</h1>
42 <ToolList />
43 </div>
44 </RegistryProvider>
45 );
46}
apps/web/src/api/atoms.ts+40−0
@@ -0,0 +1,40 @@
1import { ScopeId, ToolId, SecretId } from "@executor/sdk";
2import { ExecutorClient } from "./client";
3
4// ---------------------------------------------------------------------------
5// Query atoms — typed, cached, reactive
6// ---------------------------------------------------------------------------
7
8export const toolsAtom = (scopeId: ScopeId = ScopeId.make("default")) =>
9 ExecutorClient.query("tools", "list", {
10 path: { scopeId },
11 timeToLive: "30 seconds",
12 });
13
14export const toolSchemaAtom = (scopeId: ScopeId, toolId: ToolId) =>
15 ExecutorClient.query("tools", "schema", {
16 path: { scopeId, toolId },
17 timeToLive: "1 minute",
18 });
19
20export const secretsAtom = (scopeId: ScopeId = ScopeId.make("default")) =>
21 ExecutorClient.query("secrets", "list", {
22 path: { scopeId },
23 timeToLive: "30 seconds",
24 });
25
26export const secretStatusAtom = (scopeId: ScopeId, secretId: SecretId) =>
27 ExecutorClient.query("secrets", "status", {
28 path: { scopeId, secretId },
29 timeToLive: "15 seconds",
30 });
31
32// ---------------------------------------------------------------------------
33// Mutation atoms — fire-and-forget style
34// ---------------------------------------------------------------------------
35
36export const invokeTool = ExecutorClient.mutation("tools", "invoke");
37
38export const setSecret = ExecutorClient.mutation("secrets", "set");
39
40export const removeSecret = ExecutorClient.mutation("secrets", "remove");
apps/web/src/api/client.ts+15−0
@@ -0,0 +1,15 @@
1import { AtomHttpApi } from "@effect-atom/atom-react";
2import { FetchHttpClient } from "@effect/platform";
3import { ExecutorApi } from "@executor/api";
4
5// ---------------------------------------------------------------------------
6// Typed HTTP API client — uses effect-atom's AtomHttpApi.Tag
7// ---------------------------------------------------------------------------
8
9class ExecutorClient extends AtomHttpApi.Tag<ExecutorClient>()("ExecutorClient", {
10 api: ExecutorApi,
11 httpClient: FetchHttpClient.layer,
12 baseUrl: "/",
13}) {}
14
15export { ExecutorClient };
apps/web/src/api/index.ts+10−0
@@ -0,0 +1,10 @@
1export { ExecutorClient } from "./client";
2export {
3 toolsAtom,
4 toolSchemaAtom,
5 secretsAtom,
6 secretStatusAtom,
7 invokeTool,
8 setSecret,
9 removeSecret,
10} from "./atoms";
apps/web/src/main.tsx+6−0
@@ -0,0 +1,6 @@
1import React from "react";
2import { createRoot } from "react-dom/client";
3import { App } from "./App";
4
5const root = createRoot(document.getElementById("root")!);
6root.render(<App />);

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t