Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3fe10c5
docs: add freecode TUI design spec
May 8, 2026
ebd08f7
docs: add FreeCode TUI implementation plan and initial component stru…
May 8, 2026
0e71689
feat(tui): scaffold TUI app with Next.js
May 8, 2026
a899dae
feat(tui): add ChatLayout, Logo, and PromptInput components
May 8, 2026
868be45
feat(tui): add message components (User, Assistant, Text, Code, Tool)
May 8, 2026
7443d47
feat(tui): add Electron host app for terminal-native TUI
May 8, 2026
de92d6b
refactor(tui): switch from Next.js/web to Ink (React for CLI)
May 8, 2026
e7d8977
refactor(tui): extract logo data to separate file
May 8, 2026
3f32d8a
fix(tui): update logo to display "freeCode" correctly
May 8, 2026
a65c6b4
fix(tui): correct logo ASCII art formatting
May 8, 2026
016d3db
refactor(tui): center align logo and messages in ChatLayout, enhance …
May 8, 2026
541a10a
refactor: replace custom ink-based TUI components with pi-tui framewo…
ayan-de May 9, 2026
c334df9
feat: add ASCII logo and stylized welcome message to TUI entry point
ayan-de May 9, 2026
63d4875
feat: implement command registry and slash command support in TUI
ayan-de May 9, 2026
2d00487
feat: implement slash command system and autocomplete integration in …
ayan-de May 9, 2026
a4bc0d8
Add AVAILABLE_MODELS constant in separate file
ayan-de May 9, 2026
19a52a9
Add showModelSelector to CommandContext interface
ayan-de May 9, 2026
7ed681b
Add /model command and import AVAILABLE_MODELS
ayan-de May 9, 2026
ea2acd1
Implement model selector dropdown UI with SelectList component
ayan-de May 9, 2026
f3456ec
docs: add developer guide for project architecture and coding standards
ayan-de May 9, 2026
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
231 changes: 231 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
# FreeCode Agent Guide

> How to work on this codebase — architectural principles, patterns, and practices.

## Project Overview

FreeCode is a CLI tool that drives ChatGPT (via Playwright/CDP) to assist with coding tasks. The architecture consists of:

- **CLI Backend** (`apps/cli/`) — Node.js/TypeScript that handles browser automation, context management, response parsing, and file application
- **TUI Frontend** (`apps/tui/`) — React + xterm.js terminal UI with layered architecture (terminal rendering + React DOM overlay)

The system uses a two-phase approach: ChatGPT first returns which files it needs, then receives those files + prompt and returns structured file changes.

---

## Architectural Principles

### Core Design Principles

1. **SOLID** — Single responsibility, Open-closed, Liskov substitution, Interface segregation, Dependency inversion
2. **YAGNI** — Only implement what's needed now; avoid speculative generalization
3. **DRY** — Don't repeat yourself; extract shared logic to single sources of truth
4. **Decomposition** — Each file/module does one thing well; avoid bloated files

### React Component Guidelines

1. **Single responsibility per component** — A component should render one UI element or compose smaller components. If a component exceeds ~150 lines, decompose it.
2. **Colocation** — Keep component-specific hooks, utils, and types near the component that uses them
3. **Composition over prop-drilling** — Use compound components, context, or composition patterns instead of passing many props through many levels
4. **Extract when used in 2+ places** — If logic/JSX is copied, extract it
5. **Pure presentational vs smart containers** — Separate data-fetching from rendering

### State Management

- **Zustand stores** (`stores/`) — Global state that crosses component boundaries (chat, panels, session)
- **Local state** (`useState`) — Component-specific state that doesn't escape the component
- **Derived state** — Compute from store values, don't duplicate in store
- **Store access in non-components** — Use `store.getState()` (not hooks) for IPC, utilities, etc.

---

## Project Structure

```
freecode/
├── apps/
│ ├── cli/ # CLI backend (Node.js/TypeScript)
│ │ └── src/
│ │ ├── index.ts # Entry point
│ │ ├── cli.ts # REPL orchestration
│ │ ├── browser/ # Playwright + CDP controller
│ │ │ ├── controller.ts
│ │ │ ├── chatgpt-adapter.ts
│ │ │ └── types.ts
│ │ ├── context/ # Two-phase context engine
│ │ │ ├── engine.ts
│ │ │ └── file-tree.ts
│ │ ├── parser/ # Format-agnostic response parser
│ │ │ ├── index.ts
│ │ │ ├── json-parser.ts
│ │ │ ├── markdown-parser.ts
│ │ │ └── types.ts
│ │ ├── applier/ # File application with diff preview
│ │ │ ├── index.ts
│ │ │ ├── differ.ts
│ │ │ └── writer.ts
│ │ └── types/ # Shared types
│ └── tui/ # React TUI frontend
│ └── src/
│ ├── app/ # Next.js app router
│ ├── components/ # UI components
│ │ ├── ChatLayout.tsx
│ │ ├── PromptInput.tsx
│ │ ├── Logo.tsx
│ │ ├── messages/ # Message rendering
│ │ │ ├── UserMessage.tsx
│ │ │ ├── AssistantMessage.tsx
│ │ │ └── parts/ # Message part renderers
│ │ │ ├── TextPart.tsx
│ │ │ ├── CodePart.tsx
│ │ │ └── ToolPart.tsx
│ │ └── ui/ # LayerStack, Toast, Dialog
│ ├── stores/ # Zustand stores
│ │ ├── chat-store.ts
│ │ ├── ui-panel-store.ts
│ │ ├── session-store.ts
│ │ └── index.ts
│ ├── ipc/ # JSON-RPC bridge to CLI
│ │ ├── bridge.ts
│ │ ├── protocol.ts
│ │ └── client.ts
│ └── hooks/ # Custom hooks
│ └── useAutoResize.ts
├── packages/
│ └── shared/ # Shared types between apps
│ └── src/
│ └── types.ts
└── docs/
└── superpowers/
├── specs/ # Design specifications
└── plans/ # Implementation plans
```

---

## Component Design Patterns

### Message Parts Pattern

Messages contain typed `parts`:

```typescript
type MessagePart =
| { type: 'text'; content: string }
| { type: 'code'; language: string; content: string }
| { type: 'tool'; tool: { name: string; args: Record<string, unknown> }; result?: string }
```

Each part type has its own component (`TextPart`, `CodePart`, `ToolPart`). The parent `Message` component switches on type:

```typescript
// In AssistantMessage.tsx
{message.parts.map((part, i) => {
switch (part.type) {
case 'text': return <TextPart key={i} content={part.content} />
case 'code': return <CodePart key={i} language={part.language} content={part.content} />
case 'tool': return <ToolPart key={i} tool={part.tool} result={part.result} />
}
})}
```

### Store Pattern

Each store is in its own file with co-located types:

```typescript
// stores/chat-store.ts
interface ChatStore {
messages: Message[]
status: 'idle' | 'streaming' | 'error'
// ...
}
export const useChatStore = create<ChatStore>((set) => ({ /* ... */ }))
```

Export from `stores/index.ts` for clean imports:

```typescript
export { useChatStore, type Message, type MessagePart } from './chat-store'
```

### Hook Pattern

Custom hooks encapsulate logic and state:

```typescript
// hooks/useAutoResize.ts
export function useAutoResize(options: UseAutoResizeOptions = {}) {
const textareaRef = useRef<HTMLTextAreaElement>(null)
const resize = useCallback(() => { /* ... */ }, [])
return { textareaRef, resize }
}
```

---

## File Naming Conventions

| Type | Convention | Example |
|------|-----------|---------|
| Components | PascalCase | `ChatLayout.tsx`, `CodePart.tsx` |
| Stores | kebab-case | `chat-store.ts`, `ui-panel-store.ts` |
| Hooks | camelCase with `use` prefix | `useAutoResize.ts` |
| Utilities | camelCase | `file-tree.ts`, `differ.ts` |
| Types/Interfaces | PascalCase | `types.ts` exports `FileChange`, `ParsedResponse` |

---

## Adding New Features

### 1. Identify the domain

- **Browser layer** (`apps/cli/src/browser/`) — Playwright/CDP, DOM adapters
- **Context layer** (`apps/cli/src/context/`) — File tree, context compilation
- **Parser layer** (`apps/cli/src/parser/`) — Response parsing (JSON/markdown/tool)
- **Applier layer** (`apps/cli/src/applier/`) — File writing, diff generation
- **UI components** (`apps/tui/src/components/`) — React components

### 2. Check existing patterns

Before adding code, verify:
- Does a similar pattern exist? Follow it.
- Is this functionality needed in more than one place? Extract to shared.
- Does this component do more than one thing? Decompose.

### 3. File limits

If a file exceeds ~150 lines, decompose:
- Extract sub-components
- Move helper functions to `lib/` or `utils/`
- Split store logic into separate files

### 4. Testing

- **Components** — React Testing Library
- **Stores** — Unit tests for state transitions
- **IPC** — Integration tests with mock backend
- **E2E** — Playwright for full flow

---

## Key invariants

1. **Components are dumb** — They receive props and render UI; business logic lives in stores/hooks
2. **Stores are flat** — No nested store composition; use selectors for derived state
3. **IPC is centralized** — All TUI→backend communication goes through `ipc/client.ts`
4. **Types are shared** — Core domain types (`FileChange`, `ParsedResponse`) live in `packages/shared`
5. **DOM adapters are isolated** — ChatGPT/Claude adapters in `browser/` can be swapped without changing core logic

---

## Deferred Items (Not Yet Implemented)

- Rust TUI for richer terminal UI
- Provider adapters (Claude, Gemini)
- Context intelligence (graphify/contextcarry integration)
- VS Code extension
- Autonomous multi-step agents
- Vector DB / semantic search

Don't implement these unless explicitly requested.
6 changes: 6 additions & 0 deletions apps/tui/next-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 changes: 6 additions & 0 deletions apps/tui/next.config.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}

module.exports = nextConfig
24 changes: 24 additions & 0 deletions apps/tui/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
{
"name": "@freecode/tui",
"version": "0.1.0",
"private": true,
"type": "module",
"bin": {
"freecode": "./dist/index.js"
},
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@earendil-works/pi-tui": "^0.74.0",
"chalk": "^5.5.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"tsx": "^4.0.0",
"typescript": "^5.7.0"
}
}
10 changes: 10 additions & 0 deletions apps/tui/src/assets/logo.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
export const logoLines = [
' ██████╗ ██████╗ ███████╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗',
'██╔════╝ ██╔══██╗██╔════╝██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔════╝',
'█████╗ ██████╔╝█████╗ █████╗ ██║ ██║ ██║██║ ██║█████╗ ',
'██╔══╝ ██╔══██╗██╔══╝ ██╔══╝ ██║ ██║ ██║██║ ██║██╔══╝ ',
'██║ ██║ ██║███████╗███████╗╚██████╗╚██████╔╝██████╔╝███████╗',
'╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝╚══════╝',
]

export const logoTagline = 'AI-assisted coding — no API costs'
47 changes: 47 additions & 0 deletions apps/tui/src/commands/built-in.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
import { registerCommand, type Command, type CommandContext } from "./index.js";
import { AVAILABLE_MODELS } from "../models.js";

const helpCommand: Command = {
name: "help",
description: "Show available commands",
execute: (_args, ctx) => {
ctx.showMessage(`**Available Commands:**

- **/help** - Show this help message
- **/clear** - Clear all messages
- **/model** - Select AI model
- **/exit** - Exit FreeCode`);
},
};

const clearCommand: Command = {
name: "clear",
description: "Clear all messages",
execute: (_args, ctx) => {
ctx.showMessage("*Messages cleared*");
},
};

const exitCommand: Command = {
name: "exit",
description: "Exit FreeCode",
execute: () => {
process.exit(0);
},
};

const modelCommand: Command = {
name: "model",
description: "Select AI model",
execute: (_args, ctx) => {
ctx.showMessage(`**Select AI Model:**\n\nUse the selector below to choose a model.`);
ctx.showModelSelector?.();
},
};

export function registerBuiltInCommands(): void {
registerCommand(helpCommand);
registerCommand(clearCommand);
registerCommand(exitCommand);
registerCommand(modelCommand);
}
55 changes: 55 additions & 0 deletions apps/tui/src/commands/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import type { AutocompleteItem, SlashCommand } from "@earendil-works/pi-tui";

export interface CommandContext {
showMessage(content: string): void;
showModelSelector?(): void;
}

export interface Command {
name: string;
description: string;
execute(args: string[], context: CommandContext): void | Promise<void>;
}

class CommandRegistry {
private commands = new Map<string, Command>();
private autocompleteItems: AutocompleteItem[] = [];

register(command: Command): void {
this.commands.set(command.name, command);
this.autocompleteItems.push({
label: command.name,
value: command.name,
description: command.description,
});
}

get(name: string): Command | undefined {
return this.commands.get(name);
}

getAll(): Command[] {
return Array.from(this.commands.values());
}

getAutocompleteItems(): AutocompleteItem[] {
return this.autocompleteItems;
}

getSlashCommands(): SlashCommand[] {
return this.getAll().map((cmd) => ({
name: cmd.name,
description: cmd.description,
}));
}
}

export const commandRegistry = new CommandRegistry();

export function registerCommand(command: Command): void {
commandRegistry.register(command);
}

export function getCommand(name: string): Command | undefined {
return commandRegistry.get(name);
}
Loading