Skip to content

Commit ab085b7

Browse files
authored
feat: Update readme files and documentation (#663)
1 parent 1c31c04 commit ab085b7

5 files changed

Lines changed: 387 additions & 160 deletions

File tree

CLAUDE.md

Lines changed: 256 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
- Monorepo with pnpm workspaces and turbo
66
- `apps/twig` - Twig Electron desktop app (React + Vite)
7+
- `apps/cli` - CLI tool (thin wrapper around @twig/core)
8+
- `apps/mobile` - React Native mobile app (Expo)
79
- `packages/agent` - TypeScript agent framework wrapping Claude Agent SDK
10+
- `packages/core` - Shared business logic for jj/GitHub operations
11+
- `packages/electron-trpc` - Custom tRPC package for Electron IPC
812

913
## Commands
1014

@@ -59,13 +63,13 @@ Import directly from source files instead.
5963

6064
## Architecture
6165

62-
See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed patterns (DI, services, tRPC, state management).
66+
See [ARCHITECTURE.md](./apps/twig/ARCHITECTURE.md) for detailed patterns (DI, services, tRPC, state management).
6367

6468
### Electron App (apps/twig)
6569

6670
- **Main process** (`src/main/`) - Stateless services, tRPC routers, system I/O
6771
- **Renderer process** (`src/renderer/`) - React app, all application state
68-
- **IPC**: tRPC over Electron IPC (type-safe)
72+
- **IPC**: tRPC over Electron IPC (type-safe via @posthog/electron-trpc)
6973
- **DI**: InversifyJS in both processes (`src/main/di/`, `src/renderer/di/`)
7074
- **State**: Zustand stores in renderer only - main is stateless
7175
- **Testing**: Vitest with React Testing Library
@@ -88,22 +92,264 @@ See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed patterns (DI, services, tR
8892

8993
- Shared business logic for jj/GitHub operations
9094

95+
## Agent Integration Guidelines
96+
97+
- **No rawInput**: Don't use Claude Code SDK's `rawInput` - only use Zod validated meta fields. This keeps us agent agnostic and gives us a maintainable, extensible format for logs.
98+
- **Use ACP SDK types**: Don't roll your own types for things available in the ACP SDK. Import types directly from `@anthropic-ai/claude-agent-sdk` TypeScript SDK.
99+
- **Permissions via tool calls**: If something requires user input/approval, implement it through a tool call with a permission instead of custom methods + notifications. Avoid patterns like `_array/permission_request`.
100+
91101
## Key Libraries
92102

93-
- React 18, Radix UI Themes, Tailwind CSS
103+
- React 19, Radix UI Themes, Tailwind CSS
94104
- TanStack Query for data fetching
95105
- xterm.js for terminal emulation
96106
- CodeMirror for code editing
97107
- Tiptap for rich text
98108
- Zod for schema validation
109+
- InversifyJS for dependency injection
110+
- Sonner for toast notifications
111+
112+
## Code Patterns
113+
114+
### React Components
115+
116+
Components are functional with hooks. Props typed with interfaces:
117+
118+
```typescript
119+
interface AgentMessageProps {
120+
content: string;
121+
}
122+
123+
export function AgentMessage({ content }: AgentMessageProps) {
124+
return (
125+
<Box className="py-1 pl-3">
126+
<MarkdownRenderer content={content} />
127+
</Box>
128+
);
129+
}
130+
```
131+
132+
Complex components organize hooks by concern (data, UI state, side effects):
133+
134+
```typescript
135+
export function TaskDetail({ task: initialTask }: TaskDetailProps) {
136+
const taskId = initialTask.id;
137+
useTaskData({ taskId, initialTask }); // Data fetching
138+
139+
const workspace = useWorkspaceStore((state) => state.workspaces[taskId]); // Store
140+
const [filePickerOpen, setFilePickerOpen] = useState(false); // Local state
141+
142+
useHotkeys("mod+p", () => setFilePickerOpen(true), {...}); // Effects
143+
useFileWatcher(effectiveRepoPath ?? null, taskId);
144+
// ...
145+
}
146+
```
147+
148+
### Zustand Stores
149+
150+
Stores separate state and actions with persistence middleware:
151+
152+
```typescript
153+
interface SidebarStoreState {
154+
open: boolean;
155+
width: number;
156+
}
157+
158+
interface SidebarStoreActions {
159+
setOpen: (open: boolean) => void;
160+
toggle: () => void;
161+
}
162+
163+
type SidebarStore = SidebarStoreState & SidebarStoreActions;
164+
165+
export const useSidebarStore = create<SidebarStore>()(
166+
persist(
167+
(set) => ({
168+
open: false,
169+
width: 256,
170+
setOpen: (open) => set({ open }),
171+
toggle: () => set((state) => ({ open: !state.open })),
172+
}),
173+
{
174+
name: "sidebar-storage",
175+
partialize: (state) => ({ open: state.open, width: state.width }),
176+
}
177+
)
178+
);
179+
```
180+
181+
### tRPC Routers (Main Process)
182+
183+
Routers get services from DI container per-request:
184+
185+
```typescript
186+
const getService = () => container.get<GitService>(MAIN_TOKENS.GitService);
187+
188+
export const gitRouter = router({
189+
detectRepo: publicProcedure
190+
.input(detectRepoInput)
191+
.output(detectRepoOutput)
192+
.query(({ input }) => getService().detectRepo(input.directoryPath)),
193+
194+
onCloneProgress: publicProcedure.subscription(async function* (opts) {
195+
const service = getService();
196+
for await (const data of service.toIterable(GitServiceEvent.CloneProgress, { signal: opts.signal })) {
197+
yield data;
198+
}
199+
}),
200+
});
201+
```
202+
203+
### Services (Main Process)
204+
205+
Services are injectable, stateless, and can emit events:
206+
207+
```typescript
208+
@injectable()
209+
export class GitService extends TypedEventEmitter<GitServiceEvents> {
210+
public async detectRepo(directoryPath: string): Promise<DetectRepoResult | null> {
211+
if (!directoryPath) return null;
212+
const remoteUrl = await this.getRemoteUrl(directoryPath);
213+
// ...
214+
}
215+
}
216+
```
217+
218+
### Custom Hooks
219+
220+
Hooks extract store subscriptions into cleaner interfaces:
221+
222+
```typescript
223+
export function useConnectivity() {
224+
const isOnline = useConnectivityStore((s) => s.isOnline);
225+
const check = useConnectivityStore((s) => s.check);
226+
return { isOnline, check };
227+
}
228+
```
229+
230+
### Logger Usage
231+
232+
Use scoped logger instead of console:
233+
234+
```typescript
235+
const log = logger.scope("navigation-store");
236+
237+
export const useNavigationStore = create<NavigationStore>()(
238+
persist((set, get) => {
239+
log.info("Folder path is stale, redirecting...", { folderId: folder.id });
240+
// ...
241+
})
242+
);
243+
```
99244

100-
## Environment Variables
101-
102-
- Copy `.env.example` to `.env`
245+
## Testing
103246

104-
TODO: Update me
247+
### Commands
248+
249+
- `pnpm test` - Run unit tests across all packages
250+
- `pnpm --filter twig test` - Run twig unit tests only
251+
- `pnpm test:e2e` - Run Playwright E2E tests
252+
253+
### When to Write Unit Tests vs E2E Tests
254+
255+
**Unit tests (Vitest)** - Fast, isolated, run frequently:
256+
- Zustand store logic and state transitions
257+
- Pure utility functions and helpers
258+
- Service methods with mocked dependencies
259+
- Complex business logic in isolation
260+
- Data transformations and validators
261+
262+
**E2E tests (Playwright)** - Slower, test real user flows:
263+
- Critical user journeys (auth, task creation, workspace setup)
264+
- IPC communication between main and renderer
265+
- Features requiring real Electron APIs (file system, shell)
266+
- Multi-step workflows spanning multiple components
267+
- Regression tests for reported bugs
268+
269+
**Rule of thumb**: If it can be tested without Electron running, use a unit test. If it requires the full app context or tests user-facing behavior, use E2E.
270+
271+
### Test File Location
272+
273+
Tests are colocated with source code using `.test.ts` or `.test.tsx` extension. E2E tests live in `tests/e2e/`.
274+
275+
### Store Testing
276+
277+
```typescript
278+
describe("store", () => {
279+
beforeEach(() => {
280+
localStorage.clear();
281+
useStore.setState({ /* reset state */ });
282+
});
283+
284+
it("action changes state", () => {
285+
useStore.getState().action();
286+
expect(useStore.getState().property).toBe(expectedValue);
287+
});
288+
289+
it("persists to localStorage", () => {
290+
useStore.getState().action();
291+
const persisted = localStorage.getItem("store-key");
292+
expect(JSON.parse(persisted).state).toEqual(expectedState);
293+
});
294+
});
295+
```
296+
297+
### Mocking Patterns
298+
299+
**Hoisted mocks for complex modules:**
300+
```typescript
301+
const mockPty = vi.hoisted(() => ({ spawn: vi.fn() }));
302+
vi.mock("node-pty", () => mockPty);
303+
```
304+
305+
**Simple module mocks:**
306+
```typescript
307+
vi.mock("@renderer/lib/analytics", () => ({ track: vi.fn() }));
308+
```
309+
310+
**Global fetch stubbing:**
311+
```typescript
312+
const mockFetch = vi.fn();
313+
vi.stubGlobal("fetch", mockFetch);
314+
mockFetch.mockResolvedValueOnce(ok());
315+
```
316+
317+
### Test Helpers
318+
319+
Test utilities are in `src/test/`:
320+
- `setup.ts` - Global test setup with localStorage mock
321+
- `utils.tsx` - `renderWithProviders()` for component tests
322+
- `fixtures.ts` - Mock data factories
323+
- `panelTestHelpers.ts` - Domain-specific assertions
324+
325+
## Directory Structure
326+
327+
```
328+
apps/twig/src/
329+
├── main/
330+
│ ├── di/ # InversifyJS container + tokens
331+
│ ├── services/ # Stateless services (git, shell, workspace, etc.)
332+
│ ├── trpc/
333+
│ │ ├── router.ts # Root router combining all routers
334+
│ │ └── routers/ # Individual routers per service
335+
│ └── lib/logger.ts
336+
├── renderer/
337+
│ ├── di/ # Renderer DI container
338+
│ ├── features/ # Feature modules (sessions, tasks, terminal, etc.)
339+
│ ├── stores/ # Zustand stores (21+ stores)
340+
│ ├── hooks/ # Custom React hooks
341+
│ ├── components/ # Shared components
342+
│ ├── trpc/client.ts # tRPC client setup
343+
│ └── lib/
344+
│ ├── analytics.ts # PostHog integration
345+
│ └── logger.ts
346+
├── shared/ # Shared between main & renderer
347+
│ ├── types.ts # Shared type definitions
348+
│ └── constants.ts
349+
├── api/ # PostHog API client
350+
└── test/ # Test utilities
351+
```
105352

106-
## Testing
353+
## Environment Variables
107354

108-
- `pnpm test` - Run tests across all packages
109-
- Twig app: Vitest with jsdom, helpers in `apps/twig/src/test/`
355+
- Copy `.env.example` to `.env`

0 commit comments

Comments
 (0)