@demirarch/recode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ import { MODELS } from './models.js';
2
+ export const HELP_TEXT = `
3
+ Recode — AI Coding Agent (github.com/demirgitbuh/recode)
4
+
5
+ SLASH COMMANDS
6
+ /help Show this help
7
+ /model Change AI model
8
+ /models List all available models
9
+ /clear Clear conversation history
10
+ /exit /quit Exit Recode
11
+ /cd <path> Change working directory
12
+ /ls [path] List directory contents
13
+ /read <path> Read file and show contents
14
+ /tools List available AI tools
15
+ /cwd Show current working directory
16
+ /version Show Recode version
17
+
18
+ KEYBINDINGS
19
+ Enter Send message
20
+ Ctrl+C Exit
21
+ Ctrl+L Clear screen
22
+ `.trim();
23
+ const TOOLS_TEXT = `
24
+ AI TOOLS (called automatically by the AI)
25
+ read_file <path> Read file contents
26
+ write_file <path> <content> Create or overwrite a file
27
+ list_directory [path] List directory entries
28
+ create_directory <path> Create directory (recursive)
29
+ delete_file <path> Delete a file
30
+ execute_command <cmd> Run shell command in cwd
31
+ search_files <pattern> Search files for pattern
32
+ `.trim();
33
+ export function parseCommand(input, cwd) {
34
+ const trimmed = input.trim();
35
+ if (!trimmed.startsWith('/'))
36
+ return { type: 'none' };
37
+ const [cmd, ...rest] = trimmed.slice(1).split(' ');
38
+ const arg = rest.join(' ').trim();
39
+ switch (cmd.toLowerCase()) {
40
+ case 'help':
41
+ return { type: 'system', content: HELP_TEXT };
42
+ case 'model':
43
+ return { type: 'open_model_select' };
44
+ case 'models': {
45
+ const list = MODELS.map((m) => ` ${m.provider.padEnd(11)} ${m.label.padEnd(22)} ${m.id}`).join('\n');
46
+ return { type: 'system', content: `Available models:\n\n${list}` };
47
+ }
48
+ case 'clear':
49
+ return { type: 'clear' };
50
+ case 'exit':
51
+ case 'quit':
52
+ case 'q':
53
+ return { type: 'exit' };
54
+ case 'cd':
55
+ if (!arg)
56
+ return { type: 'system', content: `Usage: /cd <path>` };
57
+ return { type: 'cd', path: arg };
58
+ case 'ls': {
59
+ const p = arg || cwd;
60
+ return { type: 'send_to_ai', content: `List the directory: ${p}` };
61
+ }
62
+ case 'read': {
63
+ if (!arg)
64
+ return { type: 'system', content: `Usage: /read <path>` };
65
+ return { type: 'send_to_ai', content: `Read and show the file: ${arg}` };
66
+ }
67
+ case 'tools':
68
+ return { type: 'system', content: TOOLS_TEXT };
69
+ case 'cwd':
70
+ return { type: 'system', content: `Working directory: ${cwd}` };
71
+ case 'version':
72
+ return { type: 'system', content: 'Recode v0.1.0 — by DemirArch' };
73
+ default:
74
+ return {
75
+ type: 'system',
76
+ content: `Unknown command: /${cmd}\nType /help for available commands.`,
77
+ };
78
+ }
79
+ }
@@ -0,0 +1,22 @@
1
+ import { promises as fs } from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ const CONFIG_DIR = path.join(os.homedir(), 'recode-data');
5
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
6
+ export async function loadConfig() {
7
+ try {
8
+ const raw = await fs.readFile(CONFIG_PATH, 'utf-8');
9
+ const parsed = JSON.parse(raw);
10
+ if (parsed.apiKey && parsed.model) {
11
+ return { apiKey: parsed.apiKey, model: parsed.model };
12
+ }
13
+ return null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export async function saveConfig(config) {
20
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
21
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
22
+ }
@@ -0,0 +1,56 @@
1
+ export const MODELS = [
2
+ // Anthropic
3
+ { id: 'anthropic/claude-opus-4-5', label: 'Claude Opus 4.5', provider: 'Anthropic' },
4
+ { id: 'anthropic/claude-sonnet-4-5', label: 'Claude Sonnet 4.5 ★', provider: 'Anthropic' },
5
+ { id: 'anthropic/claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', provider: 'Anthropic' },
6
+ { id: 'anthropic/claude-3-5-sonnet', label: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
7
+ { id: 'anthropic/claude-3-5-haiku', label: 'Claude 3.5 Haiku', provider: 'Anthropic' },
8
+ { id: 'anthropic/claude-3-opus', label: 'Claude 3 Opus', provider: 'Anthropic' },
9
+ // OpenAI
10
+ { id: 'openai/gpt-4o', label: 'GPT-4o', provider: 'OpenAI' },
11
+ { id: 'openai/gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' },
12
+ { id: 'openai/o1', label: 'o1', provider: 'OpenAI' },
13
+ { id: 'openai/o1-mini', label: 'o1 Mini', provider: 'OpenAI' },
14
+ { id: 'openai/o3-mini', label: 'o3 Mini', provider: 'OpenAI' },
15
+ { id: 'openai/o4-mini', label: 'o4 Mini', provider: 'OpenAI' },
16
+ { id: 'openai/gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI' },
17
+ // Google
18
+ { id: 'google/gemini-2.5-pro', label: 'Gemini 2.5 Pro', provider: 'Google' },
19
+ { id: 'google/gemini-2.5-flash', label: 'Gemini 2.5 Flash', provider: 'Google' },
20
+ { id: 'google/gemini-flash-1.5', label: 'Gemini Flash 1.5', provider: 'Google' },
21
+ { id: 'google/gemini-pro-1.5', label: 'Gemini Pro 1.5', provider: 'Google' },
22
+ // Meta
23
+ { id: 'meta-llama/llama-4-maverick', label: 'Llama 4 Maverick', provider: 'Meta' },
24
+ { id: 'meta-llama/llama-4-scout', label: 'Llama 4 Scout', provider: 'Meta' },
25
+ { id: 'meta-llama/llama-3.3-70b-instruct', label: 'Llama 3.3 70B', provider: 'Meta' },
26
+ { id: 'meta-llama/llama-3.1-405b-instruct', label: 'Llama 3.1 405B', provider: 'Meta' },
27
+ // DeepSeek
28
+ { id: 'deepseek/deepseek-chat', label: 'DeepSeek Chat V3', provider: 'DeepSeek' },
29
+ { id: 'deepseek/deepseek-r1', label: 'DeepSeek R1', provider: 'DeepSeek' },
30
+ { id: 'deepseek/deepseek-r1-distill-llama-70b', label: 'DeepSeek R1 Distill 70B', provider: 'DeepSeek' },
31
+ // Mistral
32
+ { id: 'mistralai/mistral-large', label: 'Mistral Large', provider: 'Mistral' },
33
+ { id: 'mistralai/mistral-small', label: 'Mistral Small', provider: 'Mistral' },
34
+ { id: 'mistralai/codestral-latest', label: 'Codestral', provider: 'Mistral' },
35
+ { id: 'mistralai/mixtral-8x22b-instruct', label: 'Mixtral 8x22B', provider: 'Mistral' },
36
+ // Qwen
37
+ { id: 'qwen/qwen3-235b-a22b', label: 'Qwen3 235B', provider: 'Qwen' },
38
+ { id: 'qwen/qwen-2.5-72b-instruct', label: 'Qwen 2.5 72B', provider: 'Qwen' },
39
+ { id: 'qwen/qwen-2.5-coder-32b-instruct', label: 'Qwen 2.5 Coder 32B', provider: 'Qwen' },
40
+ // Cohere
41
+ { id: 'cohere/command-r-plus', label: 'Command R+', provider: 'Cohere' },
42
+ { id: 'cohere/command-r', label: 'Command R', provider: 'Cohere' },
43
+ // xAI
44
+ { id: 'x-ai/grok-3', label: 'Grok 3', provider: 'xAI' },
45
+ { id: 'x-ai/grok-3-mini', label: 'Grok 3 Mini', provider: 'xAI' },
46
+ // Microsoft
47
+ { id: 'microsoft/phi-4', label: 'Phi-4', provider: 'Microsoft' },
48
+ { id: 'microsoft/wizardlm-2-8x22b', label: 'WizardLM 2 8x22B', provider: 'Microsoft' },
49
+ ];
50
+ export const MODEL_SELECT_ITEMS = MODELS.map((m) => ({
51
+ label: `${m.provider.padEnd(11)} ${m.label}`,
52
+ value: m.id,
53
+ }));
54
+ export function getModelLabel(id) {
55
+ return MODELS.find((m) => m.id === id)?.label ?? id.split('/')[1] ?? id;
56
+ }
@@ -0,0 +1,94 @@
1
+ import { TOOL_DEFINITIONS } from './tools.js';
2
+ // ── System Prompt ─────────────────────────────────────────────
3
+ export const SYSTEM_PROMPT = `You are Recode — an autonomous AI coding agent built by DemirArch.
4
+ You run inside a terminal and have direct access to the filesystem and shell through your tools.
5
+
6
+ ━━━ IDENTITY ━━━
7
+ Name: Recode
8
+ Author: DemirArch (github.com/demirgitbuh/recode)
9
+ Interface: Terminal TUI (Ink + React + TypeScript)
10
+ Mission: Autonomous software development — from idea to working code
11
+
12
+ ━━━ CAPABILITIES ━━━
13
+ You can read, write, and create any file; execute shell commands; explore
14
+ directories; search codebases. You are capable of building entire projects
15
+ from scratch, debugging production bugs, refactoring legacy code, setting up
16
+ CI/CD, writing tests, managing git, installing packages, and more.
17
+
18
+ ━━━ CORE RULES ━━━
19
+ 1. Always complete tasks fully. Never stub, never "TODO" without implementing.
20
+ 2. Read before edit — use read_file before modifying any existing file.
21
+ 3. Explore before assuming — use list_directory on unfamiliar codebases.
22
+ 4. Verify your work — run the code, test it, check for errors with execute_command.
23
+ 5. Chain as many tools as needed to finish the job completely.
24
+ 6. Never truncate code in write_file — write the complete file every time.
25
+ 7. If ambiguous, make a reasonable assumption, state it once, and proceed.
26
+ 8. Ask at most ONE clarifying question per turn, only when truly needed.
27
+ 9. When creating multi-file projects, create ALL files before reporting done.
28
+ 10. Always create parent directories before writing files.
29
+
30
+ ━━━ OUTPUT STYLE ━━━
31
+ - Terminal display — keep lines under 80 characters when possible.
32
+ - Use fenced code blocks with language tags in explanations.
33
+ - No filler: skip "Certainly!", "Great question!", "Of course!", "Sure!".
34
+ - File operations: one line per file, brief and clear.
35
+ - Errors: show the error → root cause → fix. In that order, no padding.
36
+ - When done with a multi-step task, give a concise summary of what was built.
37
+
38
+ ━━━ CODE STANDARDS ━━━
39
+ - Write production-quality, runnable code. No placeholder logic.
40
+ - Match existing style when editing projects (indentation, naming, imports).
41
+ - Comments only for non-obvious logic. No docstring spam.
42
+ - Handle errors at system boundaries (user input, network, filesystem).
43
+ - Use the language's modern idioms and standard library features.
44
+ - Prefer explicit over clever. Prefer simple over abstract.
45
+ - Never add features the user didn't ask for.
46
+
47
+ ━━━ TOOL DISCIPLINE ━━━
48
+ read_file — call before any edit to understand current content
49
+ write_file — complete file content only; never partial or truncated
50
+ execute_command — builds, tests, git, npm, pip, cargo, go, linters, etc.
51
+ list_directory — first action when exploring an unfamiliar codebase
52
+ create_directory — call before write_file when parent dir may not exist
53
+ delete_file — only when explicitly requested or clearly needed
54
+ search_files — find definitions, usages, or patterns before assuming
55
+
56
+ ━━━ LANGUAGE EXPERTISE ━━━
57
+ You are expert-level in: TypeScript, JavaScript, Python, Rust, Go, C, C++,
58
+ C#, Java, Ruby, Swift, Kotlin, PHP, Bash, SQL, HTML, CSS, SCSS, React,
59
+ Vue, Svelte, Next.js, Express, FastAPI, Django, Axum, and more.
60
+ You know package managers: npm, pnpm, yarn, pip, cargo, go mod, apt, brew.
61
+ You know tools: git, docker, docker-compose, make, cmake, webpack, vite, esbuild.`;
62
+ // ── API Client ────────────────────────────────────────────────
63
+ export async function callOpenRouter(messages, model, apiKey) {
64
+ const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
65
+ method: 'POST',
66
+ headers: {
67
+ Authorization: `Bearer ${apiKey}`,
68
+ 'Content-Type': 'application/json',
69
+ 'HTTP-Referer': 'https://github.com/demirgitbuh/recode',
70
+ 'X-Title': 'Recode',
71
+ },
72
+ body: JSON.stringify({
73
+ model,
74
+ messages: [{ role: 'system', content: SYSTEM_PROMPT }, ...messages],
75
+ tools: TOOL_DEFINITIONS,
76
+ tool_choice: 'auto',
77
+ stream: false,
78
+ }),
79
+ });
80
+ if (!response.ok) {
81
+ const err = await response.text();
82
+ throw new Error(`OpenRouter ${response.status}: ${err}`);
83
+ }
84
+ const data = (await response.json());
85
+ if (data.error)
86
+ throw new Error(data.error.message);
87
+ const msg = data.choices?.[0]?.message;
88
+ if (!msg)
89
+ throw new Error('Empty response from OpenRouter');
90
+ return {
91
+ content: msg.content ?? null,
92
+ tool_calls: msg.tool_calls?.length ? msg.tool_calls : null,
93
+ };
94
+ }
@@ -0,0 +1,211 @@
1
+ import { promises as fs } from 'fs';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import path from 'path';
5
+ const execAsync = promisify(exec);
6
+ // ── OpenRouter tool definitions ──────────────────────────────
7
+ export const TOOL_DEFINITIONS = [
8
+ {
9
+ type: 'function',
10
+ function: {
11
+ name: 'read_file',
12
+ description: 'Read the contents of a file at the given path.',
13
+ parameters: {
14
+ type: 'object',
15
+ properties: {
16
+ path: { type: 'string', description: 'Path to the file (absolute or relative to cwd)' },
17
+ },
18
+ required: ['path'],
19
+ },
20
+ },
21
+ },
22
+ {
23
+ type: 'function',
24
+ function: {
25
+ name: 'write_file',
26
+ description: 'Write or overwrite a file with the given content. Always write complete file content.',
27
+ parameters: {
28
+ type: 'object',
29
+ properties: {
30
+ path: { type: 'string', description: 'Path to the file' },
31
+ content: { type: 'string', description: 'Full file content to write' },
32
+ },
33
+ required: ['path', 'content'],
34
+ },
35
+ },
36
+ },
37
+ {
38
+ type: 'function',
39
+ function: {
40
+ name: 'list_directory',
41
+ description: 'List files and directories at the given path.',
42
+ parameters: {
43
+ type: 'object',
44
+ properties: {
45
+ path: { type: 'string', description: 'Directory path to list (default: cwd)' },
46
+ },
47
+ required: [],
48
+ },
49
+ },
50
+ },
51
+ {
52
+ type: 'function',
53
+ function: {
54
+ name: 'create_directory',
55
+ description: 'Create a directory (and all parent directories) at the given path.',
56
+ parameters: {
57
+ type: 'object',
58
+ properties: {
59
+ path: { type: 'string', description: 'Directory path to create' },
60
+ },
61
+ required: ['path'],
62
+ },
63
+ },
64
+ },
65
+ {
66
+ type: 'function',
67
+ function: {
68
+ name: 'delete_file',
69
+ description: 'Delete a file at the given path.',
70
+ parameters: {
71
+ type: 'object',
72
+ properties: {
73
+ path: { type: 'string', description: 'Path to the file to delete' },
74
+ },
75
+ required: ['path'],
76
+ },
77
+ },
78
+ },
79
+ {
80
+ type: 'function',
81
+ function: {
82
+ name: 'execute_command',
83
+ description: 'Execute a shell command in the current working directory. Use for builds, tests, git, npm, etc.',
84
+ parameters: {
85
+ type: 'object',
86
+ properties: {
87
+ command: { type: 'string', description: 'Shell command to execute' },
88
+ timeout: { type: 'number', description: 'Timeout in milliseconds (default 30000)' },
89
+ },
90
+ required: ['command'],
91
+ },
92
+ },
93
+ },
94
+ {
95
+ type: 'function',
96
+ function: {
97
+ name: 'search_files',
98
+ description: 'Search for a text pattern in files under a directory.',
99
+ parameters: {
100
+ type: 'object',
101
+ properties: {
102
+ pattern: { type: 'string', description: 'Text or regex pattern to search for' },
103
+ directory: { type: 'string', description: 'Directory to search in (default: cwd)' },
104
+ extension: { type: 'string', description: 'File extension filter e.g. ".ts"' },
105
+ },
106
+ required: ['pattern'],
107
+ },
108
+ },
109
+ },
110
+ ];
111
+ export async function executeTool(name, args, cwd) {
112
+ const resolve = (p) => path.isAbsolute(p) ? p : path.resolve(cwd, p);
113
+ switch (name) {
114
+ case 'read_file': {
115
+ const { path: p } = args;
116
+ const content = await fs.readFile(resolve(p), 'utf-8');
117
+ const lines = content.split('\n');
118
+ const numbered = lines
119
+ .map((l, i) => `${String(i + 1).padStart(4, ' ')} ${l}`)
120
+ .join('\n');
121
+ return `File: ${p} (${lines.length} lines)\n\n${numbered}`;
122
+ }
123
+ case 'write_file': {
124
+ const { path: p, content } = args;
125
+ const full = resolve(p);
126
+ await fs.mkdir(path.dirname(full), { recursive: true });
127
+ await fs.writeFile(full, content, 'utf-8');
128
+ const lines = content.split('\n').length;
129
+ return `Written: ${p} (${lines} lines)`;
130
+ }
131
+ case 'list_directory': {
132
+ const { path: p = '.' } = args;
133
+ const entries = await fs.readdir(resolve(p), { withFileTypes: true });
134
+ const lines = entries.map((e) => {
135
+ const type = e.isDirectory() ? '/' : e.isSymbolicLink() ? '@' : ' ';
136
+ return `${type} ${e.name}`;
137
+ });
138
+ return `${resolve(p)}\n${lines.join('\n')}`;
139
+ }
140
+ case 'create_directory': {
141
+ const { path: p } = args;
142
+ await fs.mkdir(resolve(p), { recursive: true });
143
+ return `Created: ${p}`;
144
+ }
145
+ case 'delete_file': {
146
+ const { path: p } = args;
147
+ await fs.unlink(resolve(p));
148
+ return `Deleted: ${p}`;
149
+ }
150
+ case 'execute_command': {
151
+ const { command, timeout = 30000 } = args;
152
+ try {
153
+ const { stdout, stderr } = await execAsync(command, { cwd, timeout });
154
+ const out = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
155
+ return out || '(no output)';
156
+ }
157
+ catch (err) {
158
+ const e = err;
159
+ const out = [e.stdout?.trim(), e.stderr?.trim(), e.message]
160
+ .filter(Boolean)
161
+ .join('\n');
162
+ return `Exit error:\n${out}`;
163
+ }
164
+ }
165
+ case 'search_files': {
166
+ const { pattern, directory = '.', extension } = args;
167
+ const results = [];
168
+ async function walk(dir) {
169
+ let entries;
170
+ try {
171
+ entries = await fs.readdir(dir, { withFileTypes: true });
172
+ }
173
+ catch {
174
+ return;
175
+ }
176
+ for (const entry of entries) {
177
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
178
+ continue;
179
+ const full = path.join(dir, entry.name);
180
+ if (entry.isDirectory()) {
181
+ await walk(full);
182
+ }
183
+ else {
184
+ if (extension && !entry.name.endsWith(extension))
185
+ continue;
186
+ try {
187
+ const content = await fs.readFile(full, 'utf-8');
188
+ const lines = content.split('\n');
189
+ const re = new RegExp(pattern, 'i');
190
+ lines.forEach((line, idx) => {
191
+ if (re.test(line)) {
192
+ const rel = path.relative(cwd, full);
193
+ results.push(`${rel}:${idx + 1}: ${line.trim()}`);
194
+ }
195
+ });
196
+ }
197
+ catch {
198
+ // skip binary files
199
+ }
200
+ }
201
+ }
202
+ }
203
+ await walk(resolve(directory));
204
+ if (results.length === 0)
205
+ return `No matches for "${pattern}"`;
206
+ return results.slice(0, 100).join('\n');
207
+ }
208
+ default:
209
+ return `Unknown tool: ${name}`;
210
+ }
211
+ }
package/dist/main.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { render } from 'ink';
4
+ import { App } from './App.js';
5
+ const initialCwd = process.cwd();
6
+ render(_jsx(App, { initialCwd: initialCwd }));
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@demirarch/recode",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "A minimal TUI code editor with AI assistant powered by OpenRouter",
6
+ "author": "DemirArch",
7
+ "license": "SEE LICENSE IN LICENSE",
8
+ "homepage": "https://github.com/demirgitbuh/recode",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/demirgitbuh/recode.git"
12
+ },
13
+ "bin": { "recode": "./dist/main.js" },
14
+ "scripts": {
15
+ "dev": "tsx src/main.tsx",
16
+ "build": "tsc"
17
+ },
18
+ "dependencies": {
19
+ "ink": "^4.4.1",
20
+ "ink-text-input": "^5.0.1",
21
+ "ink-select-input": "^5.0.0",
22
+ "ink-spinner": "^5.0.0",
23
+ "react": "^18.2.0",
24
+ "cli-highlight": "^2.1.11",
25
+ "node-fetch": "^3.3.2"
26
+ },
27
+ "devDependencies": {
28
+ "@types/react": "^18.2.0",
29
+ "@types/node": "^20.0.0",
30
+ "typescript": "^5.0.0",
31
+ "tsx": "^4.0.0"
32
+ }
33
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,54 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Text } from 'ink';
3
+ import { loadConfig, type RecodeConfig } from './lib/config.js';
4
+ import { SetupScreen } from './components/SetupScreen.js';
5
+ import { ChatScreen } from './components/ChatScreen.js';
6
+
7
+ type AppState =
8
+ | { screen: 'loading' }
9
+ | { screen: 'setup' }
10
+ | { screen: 'chat'; config: RecodeConfig };
11
+
12
+ interface AppProps {
13
+ initialCwd: string;
14
+ }
15
+
16
+ export function App({ initialCwd }: AppProps) {
17
+ const [state, setState] = useState<AppState>({ screen: 'loading' });
18
+ const [model, setModel] = useState('');
19
+
20
+ useEffect(() => {
21
+ loadConfig().then((cfg) => {
22
+ if (cfg) {
23
+ setModel(cfg.model);
24
+ setState({ screen: 'chat', config: cfg });
25
+ } else {
26
+ setState({ screen: 'setup' });
27
+ }
28
+ });
29
+ }, []);
30
+
31
+ if (state.screen === 'loading') {
32
+ return <Text color="#F26207">Loading...</Text>;
33
+ }
34
+
35
+ if (state.screen === 'setup') {
36
+ return (
37
+ <SetupScreen
38
+ onComplete={(cfg) => {
39
+ setModel(cfg.model);
40
+ setState({ screen: 'chat', config: cfg });
41
+ }}
42
+ />
43
+ );
44
+ }
45
+
46
+ return (
47
+ <ChatScreen
48
+ model={model || state.config.model}
49
+ apiKey={state.config.apiKey}
50
+ initialCwd={initialCwd}
51
+ onModelChange={setModel}
52
+ />
53
+ );
54
+ }