Skip to content

Commit 6b38df8

Browse files
committed
Merge branch 'main' into feature/deep-link-and-oauth-refactor
2 parents 8c91e1f + 7d65ee4 commit 6b38df8

File tree

70 files changed

+2469
-1939
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+2469
-1939
lines changed

ARCHITECTURE.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# Contributing to Array
2+
3+
## Architecture Overview
4+
5+
Array is an Electron app with a React renderer. The main process handles system operations (stateless), while the renderer owns all application state.
6+
7+
```
8+
Main Process (Node.js) Renderer Process (React)
9+
┌───────────────────────┐ ┌───────────────────────────┐
10+
│ DI Container │ │ DI Container │
11+
│ ├── GitService │ │ ├── TRPCClient │
12+
│ └── ... │ │ └── TaskService, ... │
13+
├───────────────────────┤ ├───────────────────────────┤
14+
│ tRPC Routers │ ◄─tRPC(ipcLink)─► │ tRPC Clients │
15+
│ (use DI services) │ │ ├── trpcReact (hooks) │
16+
├───────────────────────┤ │ └── trpcVanilla │
17+
│ System I/O │ ├───────────────────────────┤
18+
│ (fs, git, shell) │ │ Zustand Stores (state) │
19+
│ STATELESS │ │ ├── taskStore │
20+
└───────────────────────┘ │ ├── workspaceStore │
21+
│ └── ... │
22+
├───────────────────────────┤
23+
│ React UI │
24+
└───────────────────────────┘
25+
```
26+
27+
**Key points:**
28+
- Both processes use InversifyJS for DI
29+
- Renderer DI holds services + tRPC client; services can coordinate stores
30+
- Zustand stores own all application state (not in DI)
31+
- Main process is stateless - pure I/O operations only
32+
33+
## Dependency Injection
34+
35+
Both processes use [InversifyJS](https://inversify.io/) for dependency injection with singleton scope.
36+
37+
| Process | Container | Holds |
38+
|----------|--------------------|------------------------------------- |
39+
| Main | `src/main/di/` | Stateless services (GitService, etc.)|
40+
| Renderer | `src/renderer/di/` | Services + TRPCClient |
41+
42+
### Defining a Service
43+
44+
```typescript
45+
// src/main/services/my-service/service.ts (or src/renderer/services/)
46+
import { injectable } from "inversify";
47+
48+
@injectable()
49+
export class MyService {
50+
doSomething() {
51+
// ...
52+
}
53+
}
54+
```
55+
56+
### Registering a Service
57+
58+
```typescript
59+
// src/main/di/container.ts (or src/renderer/di/container.ts)
60+
container.bind<MyService>(TOKENS.MyService).to(MyService);
61+
```
62+
63+
```typescript
64+
// src/main/di/tokens.ts (or src/renderer/di/tokens.ts)
65+
export const MAIN_TOKENS = Object.freeze({
66+
MyService: Symbol.for("Main.MyService"),
67+
});
68+
```
69+
70+
### Using a Service
71+
72+
```typescript
73+
import { get } from "@main/di"; // or @renderer/di
74+
75+
const myService = get<MyService>(TOKENS.MyService);
76+
myService.doSomething();
77+
```
78+
79+
## IPC via tRPC
80+
81+
We use [tRPC](https://trpc.io/) with [trpc-electron](https://github.com/jsonnull/electron-trpc) for type-safe communication between main and renderer. The `ipcLink()` handles serialization over Electron IPC.
82+
83+
### Creating a Router (Main Process)
84+
85+
```typescript
86+
// src/main/trpc/routers/my-router.ts
87+
import { z } from "zod";
88+
import { router, publicProcedure } from "../trpc";
89+
import { get } from "@main/di";
90+
import { MAIN_TOKENS } from "@main/di/tokens";
91+
92+
export const myRouter = router({
93+
// Query - for read operations
94+
getData: publicProcedure
95+
.input(z.object({ id: z.string() }))
96+
.query(async ({ input }) => {
97+
const service = get<MyService>(MAIN_TOKENS.MyService);
98+
return service.getData(input.id);
99+
}),
100+
101+
// Mutation - for write operations
102+
updateData: publicProcedure
103+
.input(z.object({ id: z.string(), value: z.string() }))
104+
.mutation(async ({ input }) => {
105+
const service = get<MyService>(MAIN_TOKENS.MyService);
106+
return service.updateData(input.id, input.value);
107+
}),
108+
});
109+
```
110+
111+
### Registering the Router
112+
113+
```typescript
114+
// src/main/trpc/router.ts
115+
import { myRouter } from "./routers/my-router";
116+
117+
export const trpcRouter = router({
118+
my: myRouter,
119+
// ...
120+
});
121+
```
122+
123+
### Using tRPC in Renderer
124+
125+
**React hooks:**
126+
127+
```typescript
128+
import { trpcReact } from "@renderer/trpc/client";
129+
130+
function MyComponent() {
131+
// Queries
132+
const { data } = trpcReact.my.getData.useQuery({ id: "123" });
133+
134+
// Mutations
135+
const mutation = trpcReact.my.updateData.useMutation();
136+
const handleUpdate = () => mutation.mutate({ id: "123", value: "new" });
137+
}
138+
```
139+
140+
**Outside React (vanilla client):**
141+
142+
```typescript
143+
import { trpcVanilla } from "@renderer/trpc/client";
144+
145+
const data = await trpcVanilla.my.getData.query({ id: "123" });
146+
```
147+
148+
## State Management
149+
150+
**All application state lives in the renderer.** Main process services should be stateless/pure.
151+
152+
| Layer | State | Role |
153+
|-------|-------|------|
154+
| **Renderer** | Zustand stores | Owns all application state |
155+
| **Main** | Stateless | Pure operations (file I/O, git, shell, etc.) |
156+
157+
This keeps state predictable, easy to debug, and naturally supports patterns like undo/rollback.
158+
159+
### Example
160+
161+
```typescript
162+
// ❌ Bad - main service with state
163+
@injectable()
164+
class TaskService {
165+
private currentTask: Task | null = null; // Don't do this
166+
}
167+
168+
// ✅ Good - main service is pure
169+
@injectable()
170+
class TaskService {
171+
async readTask(id: string): Promise<Task> { /* ... */ }
172+
async writeTask(task: Task): Promise<void> { /* ... */ }
173+
}
174+
175+
// ✅ Good - state lives in renderer
176+
// src/renderer/stores/task-store.ts
177+
const useTaskStore = create<TaskState>((set) => ({
178+
currentTask: null,
179+
setCurrentTask: (task) => set({ currentTask: task }),
180+
}));
181+
```
182+
183+
## Services
184+
185+
Services encapsulate business logic and exist in both processes:
186+
187+
- **Main services** (`src/main/services/`) - System operations (file I/O, git, shell)
188+
- **Renderer services** (`src/renderer/services/`) - UI logic, API calls
189+
190+
Main services should be:
191+
192+
- **Injectable**: Decorated with `@injectable()` for DI
193+
- **Stateless**: No mutable instance state, pure operations only
194+
- **Single responsibility**: One concern per service
195+
196+
### Service Structure
197+
198+
```
199+
src/main/services/
200+
├── my-service/
201+
│ ├── service.ts # The injectable service class
202+
│ └── types.ts # Types and interfaces
203+
204+
src/renderer/services/
205+
├── my-service.ts # Renderer-side service
206+
```
207+
208+
## Adding a New Feature
209+
210+
1. **Create the service** in `src/main/services/`
211+
2. **Add DI token** in `src/main/di/tokens.ts`
212+
3. **Register service** in `src/main/di/container.ts`
213+
4. **Create tRPC router** in `src/main/trpc/routers/`
214+
5. **Add router** to `src/main/trpc/router.ts`
215+
6. **Use in renderer** via `trpcReact` hooks
216+
217+
## Code Style
218+
219+
See [CLAUDE.md](./CLAUDE.md) for linting, formatting, and import conventions.
220+
221+
Key points:
222+
- Use path aliases (`@main/*`, `@renderer/*`, etc.)
223+
- No barrel files - import directly from source
224+
- Use `logger` instead of `console.*`

CLAUDE.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,28 @@
4242
- TypeScript strict mode enabled
4343
- Tailwind CSS classes should be sorted (biome `useSortedClasses` rule)
4444

45+
### Avoid Barrel Files
46+
47+
Barrel files:
48+
- Break tree-shaking
49+
- Create circular dependency risks
50+
- Hide the true source of imports
51+
- Make refactoring harder
52+
53+
Import directly from source files instead.
54+
4555
## Architecture
4656

57+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed patterns (DI, services, tRPC, state management).
58+
4759
### Electron App (apps/array)
4860

49-
- Main process: `src/main/` - Node.js, IPC handlers, services
50-
- Renderer process: `src/renderer/` - React app
51-
- Preload script: `src/main/preload.ts`
52-
- IPC bridge pattern between main/renderer
53-
- State management: Zustand stores in `src/renderer/stores/`
54-
- Testing: Vitest with React Testing Library
61+
- **Main process** (`src/main/`) - Stateless services, tRPC routers, system I/O
62+
- **Renderer process** (`src/renderer/`) - React app, all application state
63+
- **IPC**: tRPC over Electron IPC (type-safe)
64+
- **DI**: InversifyJS in both processes (`src/main/di/`, `src/renderer/di/`)
65+
- **State**: Zustand stores in renderer only - main is stateless
66+
- **Testing**: Vitest with React Testing Library
5567

5668
### Agent Package (packages/agent)
5769

apps/array/knip.json

Lines changed: 0 additions & 19 deletions
This file was deleted.

apps/array/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
"build-icons": "bash scripts/generate-icns.sh",
2222
"typecheck": "tsc -p tsconfig.node.json --noEmit && tsc -p tsconfig.web.json --noEmit",
2323
"generate-client": "tsx scripts/update-openapi-client.ts",
24-
"knip": "knip",
2524
"test": "vitest run",
2625
"postinstall": "cd ../.. && npx @electron/rebuild -f -m node_modules/node-pty || true"
2726
},
@@ -55,7 +54,6 @@
5554
"electron": "^30.0.0",
5655
"husky": "^9.1.7",
5756
"jsdom": "^26.0.0",
58-
"knip": "^5.66.3",
5957
"lint-staged": "^15.5.2",
6058
"postcss": "^8.4.33",
6159
"tailwindcss": "^3.4.1",
@@ -130,6 +128,7 @@
130128
"file-icon": "^6.0.0",
131129
"idb-keyval": "^6.2.2",
132130
"inversify": "^7.10.6",
131+
"immer": "^11.0.1",
133132
"is-glob": "^4.0.3",
134133
"micromatch": "^4.0.5",
135134
"node-addon-api": "^8.5.0",

apps/array/src/api/posthogClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { StoredLogEntry } from "@features/sessions/utils/parseSessionLogs";
21
import type { AgentEvent } from "@posthog/agent";
32
import { logger } from "@renderer/lib/logger";
43
import type { Task, TaskRun } from "@shared/types";
4+
import type { StoredLogEntry } from "@shared/types/session-events";
55
import { buildApiFetcher } from "./fetcher";
66
import { createApiClient, type Schemas } from "./generated";
77

apps/array/src/main/index.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
declare const __BUILD_COMMIT__: string | undefined;
2+
declare const __BUILD_DATE__: string | undefined;
3+
14
import "reflect-metadata";
25
import dns from "node:dns";
36
import { mkdirSync } from "node:fs";
7+
import os from "node:os";
48
import path from "node:path";
59
import { fileURLToPath } from "node:url";
610
import {
711
app,
812
BrowserWindow,
13+
clipboard,
14+
dialog,
915
ipcMain,
1016
Menu,
1117
type MenuItemConstructorOptions,
@@ -171,7 +177,38 @@ function createWindow(): void {
171177
{
172178
label: "Array",
173179
submenu: [
174-
{ role: "about" },
180+
{
181+
label: "About Array",
182+
click: () => {
183+
const commit = __BUILD_COMMIT__ ?? "dev";
184+
const buildDate = __BUILD_DATE__ ?? "dev";
185+
const info = [
186+
`Version: ${app.getVersion()}`,
187+
`Commit: ${commit}`,
188+
`Date: ${buildDate}`,
189+
`Electron: ${process.versions.electron}`,
190+
`Chromium: ${process.versions.chrome}`,
191+
`Node.js: ${process.versions.node}`,
192+
`V8: ${process.versions.v8}`,
193+
`OS: ${process.platform} ${process.arch} ${os.release()}`,
194+
].join("\n");
195+
196+
dialog
197+
.showMessageBox({
198+
type: "info",
199+
title: "About Array",
200+
message: "Array",
201+
detail: info,
202+
buttons: ["Copy", "OK"],
203+
defaultId: 1,
204+
})
205+
.then((result) => {
206+
if (result.response === 0) {
207+
clipboard.writeText(info);
208+
}
209+
});
210+
},
211+
},
175212
{ type: "separator" },
176213
{
177214
label: "Check for Updates...",

0 commit comments

Comments
 (0)