Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": {
"editor.defaultFormatter": "biomejs.biome"
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

## Tech Stack

Bun 1.2+, TypeScript 5.9, Effect 3.21, React 19, Vite 8, Vitest 4, Tailwind CSS
Bun 1.2+, TypeScript 5.9, Effect 4-beta, React 19, Vite 8, Vitest 4, Tailwind CSS
4, Biome 2.4

## Code Style
Expand Down Expand Up @@ -68,7 +68,7 @@ commits in these repos to ensure the LLM references current code:
If any of the folders are missing (they are git ignored), clone them into
`reference/`:

- `https://github.com/Effect-TS/effect.git` -> `.reference/effect/`
- `https://github.com/Effect-TS/effect-smol.git` -> `.reference/effect/`
- `https://github.com/Effect-TS/effect-atom.git` -> `.reference/effect-atom/`

---
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ bun format
bun lint

# Type check
bun type-check
bun run type-check
```

### Testing
Expand Down
4 changes: 2 additions & 2 deletions apps/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ React frontend built with Vite and TypeScript, part of the
## Stack

- **React 19** - UI framework
- **Vite** - Build tool and dev server
- **Vite 8** - Build tool and dev server
- **TypeScript** - Type safety
- **Effect** - Functional programming utilities
- **Effect 4-beta** - Functional programming utilities
- **@repo/domain** - Shared types and schemas

## Getting Started
Expand Down
2 changes: 1 addition & 1 deletion apps/client/biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.11/schema.json",
"root": false,
"extends": "//",
"linter": {
Expand Down
26 changes: 11 additions & 15 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,38 @@
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@effect-atom/atom-react": "^0.5.0",
"@effect/experimental": "^0.60.0",
"@effect/platform": "^0.96.0",
"@effect/platform-browser": "^0.76.0",
"@effect/rpc": "^0.75.0",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@effect/atom-react": "4.0.0-beta.47",
"@effect/platform-browser": "4.0.0-beta.47",
"@repo/domain": "workspace:*",
"@tailwindcss/vite": "^4.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"effect": "3.21.0",
"lucide-react": "^1.0.1",
"effect": "4.0.0-beta.47",
"lucide-react": "^1.8.0",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-markdown": "^10.1.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"shadcn": "^4.1.0",
"shadcn": "^4.2.0",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@effect/language-service": "^0.81.0",
"@repo/config-typescript": "workspace:*",
"@testing-library/jest-dom": "^6.9.1",
"@types/bun": "^1.3.11",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/browser": "^4.1.1",
"@vitest/browser-playwright": "^4.1.1",
"@vitest/browser": "^4.1.4",
"@vitest/browser-playwright": "^4.1.4",
"typescript": "~6.0.2",
"vite": "^8.0.2",
"vite": "^8.0.8",
"vitest": "^4.1.0",
"vitest-browser-react": "^2.1.0"
"vitest-browser-react": "^2.2.0"
}
}
25 changes: 12 additions & 13 deletions apps/client/src/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,37 @@ import { describe, expect, test, vi } from "vitest";
import { render } from "vitest-browser-react";
import App from "./app";

// Mock the atom hooks to avoid real API calls
vi.mock("@effect-atom/atom-react", () => ({
Result: {
// Mock the atom hooks (v4: @effect/atom-react)
vi.mock("@effect/atom-react", () => ({
useAtom: vi.fn(() => [{ _tag: "Initial" }, vi.fn()]),
useAtomSet: vi.fn(() => vi.fn()),
}));

// Mock AsyncResult from effect/unstable/reactivity
vi.mock("effect/unstable/reactivity", () => ({
AsyncResult: {
getOrElse: vi.fn((_result: unknown, fallback: () => unknown) => {
return fallback();
}),
match: vi.fn(
(_result: unknown, handlers: Record<string, () => unknown>) => {
return handlers["onInitial"] ? handlers["onInitial"]() : null;
},
),
builder: vi.fn(() => ({
onSuccess: vi.fn().mockReturnThis(),
onFailure: vi.fn().mockReturnThis(),
onInitial: vi.fn().mockReturnThis(),
orNull: vi.fn(() => null),
})),
match: vi.fn((_result: unknown, _handlers: unknown) => null),
isSuccess: vi.fn(() => false),
isInitial: vi.fn(() => true),
isFailure: vi.fn(() => false),
isWaiting: vi.fn(() => false),
},
useAtom: vi.fn(() => [{ _tag: "Initial" }, vi.fn()]),
useAtomSet: vi.fn(() => vi.fn()),
}));

vi.mock("./lib/atom", () => ({
runtime: { fn: vi.fn(() => vi.fn()) },
helloAtom: vi.fn(),
tickAtom: vi.fn(),
runtime: {
fn: vi.fn(() => vi.fn()),
},
chatAtom: vi.fn(),
}));

vi.mock("./lib/web-socket-client", () => ({
Expand Down
11 changes: 6 additions & 5 deletions apps/client/src/components/chat-box.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Result, useAtom } from "@effect-atom/atom-react";
import { useAtom } from "@effect/atom-react";
import type { ChatResponse, MessageSegment } from "@repo/domain/Chat";
import { AsyncResult } from "effect/unstable/reactivity";
import { AlertCircle, Loader2, Send } from "lucide-react";
import { type FC, useEffect, useMemo, useRef, useState } from "react";
import { chatAtom } from "@/lib/atoms/chat-atom";
Expand Down Expand Up @@ -46,16 +47,16 @@ export function ChatBox() {
const readinessAttemptRef = useRef(0);
const lastCompletionKeyRef = useRef<string | null>(null);

const currentResult: ChatResponse = Result.getOrElse(
const currentResult: ChatResponse = AsyncResult.getOrElse(
result,
() => ({ _tag: "initial" }) as const,
);

const currentSegments =
currentResult._tag === "initial" ? [] : currentResult.segments;

const isWaiting = Result.isWaiting(result);
const isFailure = Result.isFailure(result);
const isWaiting = AsyncResult.isWaiting(result);
const isFailure = AsyncResult.isFailure(result);
const isStreaming = currentResult._tag === "streaming";
const sendMessages = (
messages: Array<{
Expand Down Expand Up @@ -341,7 +342,7 @@ const historyToMessages = (messages: Message[]) =>
});

const ErrorDisplay: FC<{
result: Result.Failure<unknown, unknown>;
result: AsyncResult.Failure<unknown, unknown>;
}> = ({ result }) => {
return (
<div className="flex w-full justify-center">
Expand Down
13 changes: 7 additions & 6 deletions apps/client/src/components/presence-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Result, useAtom, useAtomSet } from "@effect-atom/atom-react";
import { useAtom, useAtomSet } from "@effect/atom-react";
import type {
ClientId,
ClientInfo,
ClientStatus,
WebSocketEvent,
} from "@repo/domain/WebSocket";
import { AsyncResult } from "effect/unstable/reactivity";
import { useEffect, useMemo } from "react";
import { Badge } from "@/components/ui/badge";
import {
Expand Down Expand Up @@ -78,7 +79,7 @@ export function PresencePanel({ className }: { className?: string }) {
startSubscription();
}, [startSubscription]);

const events = Result.getOrElse(
const events = AsyncResult.getOrElse(
eventsResult,
() => [] as readonly WebSocketEvent[],
);
Expand Down Expand Up @@ -115,9 +116,9 @@ export function PresencePanel({ className }: { className?: string }) {
}
};

const isConnected = Result.isSuccess(eventsResult);
const isConnecting = Result.isInitial(eventsResult);
const hasError = Result.isFailure(eventsResult);
const isConnected = AsyncResult.isSuccess(eventsResult);
const isConnecting = AsyncResult.isInitial(eventsResult);
const hasError = AsyncResult.isFailure(eventsResult);

return (
<Card className={cn("h-full", className)}>
Expand Down Expand Up @@ -149,7 +150,7 @@ export function PresencePanel({ className }: { className?: string }) {
{/* Error Display */}
<CardContent className="flex flex-col gap-4">
{hasError &&
Result.match(eventsResult, {
AsyncResult.match(eventsResult, {
onInitial: () => null,
onSuccess: () => null,
onFailure: (error) => (
Expand Down
11 changes: 4 additions & 7 deletions apps/client/src/components/rest-card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Result, useAtom } from "@effect-atom/atom-react";
import { useAtom } from "@effect/atom-react";
import { AsyncResult } from "effect/unstable/reactivity";
import { helloAtom } from "@/lib/atoms/hello-atom";
import { Button } from "./ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
Expand All @@ -22,7 +23,7 @@ export const RestCard = () => {
</Button>
</CardContent>
</Card>
{Result.builder(response)
{AsyncResult.builder(response)
.onSuccess((data) => (
<ResponseCard
state="completed"
Expand All @@ -45,11 +46,7 @@ export const RestCard = () => {
className="flex-1"
>
<pre>
<code>
Error: {error._tag}
{"\n"}
Details: {JSON.stringify(error ?? {}, null, 2)}
</code>
<code>Error: {JSON.stringify(error, null, 2)}</code>
</pre>
</ResponseCard>
))
Expand Down
5 changes: 3 additions & 2 deletions apps/client/src/components/rpc-card.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Result, useAtom } from "@effect-atom/atom-react";
import { useAtom } from "@effect/atom-react";
import { AsyncResult } from "effect/unstable/reactivity";
import { tickAtom } from "@/lib/atoms/tick-atom";
import { Button } from "./ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { ResponseCard } from "./ui/response-card";

export const RpcCard = () => {
const [result, search] = useAtom(tickAtom);
const event = Result.getOrElse(result, () => null);
const event = AsyncResult.getOrElse(result, () => null);

const handleSearch = () => {
search({ abort: false });
Expand Down
6 changes: 3 additions & 3 deletions apps/client/src/lib/atom.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { DevTools } from "@effect/experimental";
import { Atom } from "@effect-atom/atom-react";
import { Layer } from "effect";
import { DevTools } from "effect/unstable/devtools";
import { Atom } from "effect/unstable/reactivity";
import { RpcClient } from "./rpc-client";

const ENABLE_DEVTOOLS = import.meta.env.VITE_ENABLE_DEVTOOLS === "true";

export const runtime = Atom.runtime(
RpcClient.Default.pipe(
RpcClient.layer.pipe(
Layer.provideMerge(ENABLE_DEVTOOLS ? DevTools.layer() : Layer.empty),
),
);
4 changes: 2 additions & 2 deletions apps/client/src/lib/atoms/chat-atom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Atom } from "@effect-atom/atom-react";
import type { ChatMessage, ChatResponse, ToolCall } from "@repo/domain/Chat";
import { Effect, Stream } from "effect";
import type { Atom } from "effect/unstable/reactivity";
import { runtime } from "../atom";
import { RpcClient } from "../rpc-client";

Expand Down Expand Up @@ -242,7 +242,7 @@ export const chatAtom: Atom.AtomResultFn<
},
),
Stream.drop(1),
Stream.catchAll((error: unknown) => {
Stream.catch((error: unknown) => {
console.error("[chatAtom] Caught unhandled stream error:", error);
const errorMessage =
error instanceof Error
Expand Down
12 changes: 4 additions & 8 deletions apps/client/src/lib/atoms/hello-atom.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { FetchHttpClient, HttpApiClient } from "@effect/platform";
import type { Atom } from "@effect-atom/atom-react";
import { Api, type ApiResponse } from "@repo/domain/Api";
import { Api } from "@repo/domain/Api";
import { Effect } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import { HttpApiClient } from "effect/unstable/httpapi";
import { runtime } from "../atom";

const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:9000";

export const helloAtom: Atom.AtomResultFn<
void,
typeof ApiResponse.Type,
unknown
> = runtime.fn(() =>
export const helloAtom = runtime.fn(() =>
Effect.gen(function* () {
const client = yield* HttpApiClient.make(Api, {
baseUrl: SERVER_URL,
Expand Down
Loading
Loading