@axeai/axe 0.0.1-beta

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,76 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ const CONFIG_DIR = join(homedir(), ".axe");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+
8
+ export type ProviderName =
9
+ | "google"
10
+ | "openai"
11
+ | "anthropic"
12
+ | "groq"
13
+ | "xai"
14
+ | "deepseek"
15
+ | "qwen"
16
+ | "kimi"
17
+ | "gemini"
18
+ | "minimax";
19
+
20
+ export type Config = {
21
+ provider: ProviderName;
22
+ model: string;
23
+ keys: Partial<Record<ProviderName, string>>;
24
+ };
25
+
26
+ const DEFAULT_CONFIG: Config = {
27
+ provider: "google",
28
+ model: "gemini-2.5-flash",
29
+ keys: {},
30
+ };
31
+
32
+ export function ensureConfigDir(): void {
33
+ if (!existsSync(CONFIG_DIR)) {
34
+ mkdirSync(CONFIG_DIR, { recursive: true });
35
+ }
36
+ }
37
+
38
+ export function configExists(): boolean {
39
+ return existsSync(CONFIG_FILE);
40
+ }
41
+
42
+ export function loadConfig(): Config {
43
+ ensureConfigDir();
44
+ if (!configExists()) {
45
+ return DEFAULT_CONFIG;
46
+ }
47
+ try {
48
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
49
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
50
+ } catch {
51
+ return DEFAULT_CONFIG;
52
+ }
53
+ }
54
+
55
+ export function saveConfig(config: Config): void {
56
+ ensureConfigDir();
57
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
58
+ }
59
+
60
+ export function getApiKey(provider: ProviderName): string | undefined {
61
+ const config = loadConfig();
62
+ return config.keys[provider];
63
+ }
64
+
65
+ export function setApiKey(provider: ProviderName, key: string): void {
66
+ const config = loadConfig();
67
+ config.keys[provider] = key;
68
+ saveConfig(config);
69
+ }
70
+
71
+ export function setProvider(provider: ProviderName, model: string): void {
72
+ const config = loadConfig();
73
+ config.provider = provider;
74
+ config.model = model;
75
+ saveConfig(config);
76
+ }
package/src/lib/db.ts ADDED
@@ -0,0 +1,135 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { createHash } from "crypto";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { mkdirSync, existsSync } from "fs";
6
+
7
+ const AXE_DIR = join(homedir(), ".axe");
8
+ if (!existsSync(AXE_DIR)) {
9
+ mkdirSync(AXE_DIR, { recursive: true });
10
+ }
11
+
12
+ const DB_PATH = join(AXE_DIR, "chat.db");
13
+ const db = new Database(DB_PATH);
14
+
15
+ const currentDir = process.cwd();
16
+
17
+ let currentSessionId = createHash("sha256").update(currentDir).digest("hex").slice(0, 16);
18
+
19
+ export function getSessionId(): string {
20
+ return currentSessionId;
21
+ }
22
+
23
+ export function setSessionId(id: string) {
24
+ currentSessionId = id;
25
+ }
26
+
27
+ db.run(`
28
+ CREATE TABLE IF NOT EXISTS sessions (
29
+ id TEXT PRIMARY KEY,
30
+ path TEXT NOT NULL,
31
+ name TEXT,
32
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
33
+ last_message_at DATETIME
34
+ )
35
+ `);
36
+
37
+ db.run(`
38
+ CREATE TABLE IF NOT EXISTS messages (
39
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
40
+ session_id TEXT NOT NULL,
41
+ role TEXT NOT NULL,
42
+ content TEXT NOT NULL,
43
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
44
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
45
+ )
46
+ `);
47
+
48
+ db.run(`CREATE INDEX IF NOT EXISTS idx_session ON messages(session_id)`);
49
+
50
+ function ensureSession(sessionId: string, path: string) {
51
+ const existing = db.query("SELECT id FROM sessions WHERE id = ?").get(sessionId);
52
+ if (!existing) {
53
+ db.run("INSERT INTO sessions (id, path) VALUES (?, ?)", [sessionId, path]);
54
+ }
55
+ }
56
+
57
+ export type Message = {
58
+ id: number;
59
+ session_id: string;
60
+ role: "user" | "assistant" | "system";
61
+ content: string;
62
+ created_at: string;
63
+ };
64
+
65
+ export type Session = {
66
+ id: string;
67
+ path: string;
68
+ name: string | null;
69
+ message_count: number;
70
+ last_message_at: string;
71
+ };
72
+
73
+ export function saveMessage(role: "user" | "assistant" | "system", content: string) {
74
+ ensureSession(currentSessionId, currentDir);
75
+ db.run("INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)", [
76
+ currentSessionId,
77
+ role,
78
+ content,
79
+ ]);
80
+ db.run("UPDATE sessions SET last_message_at = CURRENT_TIMESTAMP WHERE id = ?", [currentSessionId]);
81
+ }
82
+
83
+ export function getRecentMessages(limit: number = 50): Message[] {
84
+ const query = db.query(
85
+ "SELECT * FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?"
86
+ );
87
+ const messages = query.all(currentSessionId, limit) as Message[];
88
+ return messages.reverse();
89
+ }
90
+
91
+ export function getCurrentDirSessions(): Session[] {
92
+ const query = db.query(`
93
+ SELECT s.id, s.path, s.name, s.last_message_at,
94
+ (SELECT COUNT(*) FROM messages WHERE session_id = s.id) as message_count
95
+ FROM sessions s
96
+ WHERE s.path = ?
97
+ ORDER BY s.last_message_at DESC
98
+ `);
99
+ return query.all(currentDir) as Session[];
100
+ }
101
+
102
+ export function getOtherDirSessions(): Session[] {
103
+ const query = db.query(`
104
+ SELECT s.id, s.path, s.name, s.last_message_at,
105
+ (SELECT COUNT(*) FROM messages WHERE session_id = s.id) as message_count
106
+ FROM sessions s
107
+ WHERE s.path != ?
108
+ ORDER BY s.last_message_at DESC
109
+ `);
110
+ return query.all(currentDir) as Session[];
111
+ }
112
+
113
+ export function getSessionMessages(sessionId: string, limit: number = 50): Message[] {
114
+ const query = db.query(
115
+ "SELECT * FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?"
116
+ );
117
+ const messages = query.all(sessionId, limit) as Message[];
118
+ return messages.reverse();
119
+ }
120
+
121
+ export function createNewSession(): string {
122
+ const newId = createHash("sha256")
123
+ .update(currentDir + Date.now().toString())
124
+ .digest("hex")
125
+ .slice(0, 16);
126
+
127
+ // Count existing sessions in this directory to generate a name
128
+ const countQuery = db.query("SELECT COUNT(*) as count FROM sessions WHERE path = ?");
129
+ const result = countQuery.get(currentDir) as { count: number };
130
+ const sessionName = `Session ${result.count + 1}`;
131
+
132
+ db.run("INSERT INTO sessions (id, path, name) VALUES (?, ?, ?)", [newId, currentDir, sessionName]);
133
+ currentSessionId = newId;
134
+ return newId;
135
+ }
@@ -0,0 +1,83 @@
1
+ import { readdirSync, statSync, readFileSync, existsSync } from "fs";
2
+ import { join, relative } from "path";
3
+
4
+ const DEFAULT_IGNORE = [
5
+ "node_modules",
6
+ ".git",
7
+ "dist",
8
+ "build",
9
+ "coverage",
10
+ ".next",
11
+ ".cache",
12
+ ".vscode",
13
+ ".idea",
14
+ ".DS_Store",
15
+ "bun.lock",
16
+ "package-lock.json",
17
+ "yarn.lock",
18
+ "pnpm-lock.yaml"
19
+ ];
20
+
21
+ function parseGitIgnore(dir: string): string[] {
22
+ const gitignorePath = join(dir, ".gitignore");
23
+ if (!existsSync(gitignorePath)) return [];
24
+
25
+ try {
26
+ const content = readFileSync(gitignorePath, "utf-8");
27
+ return content
28
+ .split("\n")
29
+ .map((line) => line.trim())
30
+ .filter((line) => line && !line.startsWith("#"));
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ function isIgnored(path: string, ignorePatterns: string[]): boolean {
37
+ const fileName = path.split("/").pop() || "";
38
+
39
+ if (DEFAULT_IGNORE.some(pattern => path.includes(pattern))) return true;
40
+
41
+ for (const pattern of ignorePatterns) {
42
+ if (pattern.endsWith("/")) {
43
+ const dirPattern = pattern.slice(0, -1);
44
+ if (path.includes(dirPattern)) return true;
45
+ } else if (pattern.startsWith("*")) {
46
+ const ext = pattern.slice(1);
47
+ if (fileName.endsWith(ext)) return true;
48
+ } else {
49
+ if (path.includes(pattern)) return true;
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+
55
+ export function getAllFiles(dir: string = process.cwd()): string[] {
56
+ const files: string[] = [];
57
+ const ignorePatterns = parseGitIgnore(dir);
58
+
59
+ function walk(currentDir: string) {
60
+ try {
61
+ const entries = readdirSync(currentDir);
62
+
63
+ for (const entry of entries) {
64
+ const fullPath = join(currentDir, entry);
65
+ const relPath = relative(dir, fullPath);
66
+
67
+ if (isIgnored(relPath, ignorePatterns)) continue;
68
+
69
+ const stat = statSync(fullPath);
70
+
71
+ if (stat.isDirectory()) {
72
+ walk(fullPath);
73
+ } else {
74
+ files.push(relPath);
75
+ }
76
+ }
77
+ } catch (e) {
78
+ }
79
+ }
80
+
81
+ walk(dir);
82
+ return files;
83
+ }
@@ -0,0 +1,38 @@
1
+ export const systemprompt = `You are Axe, an advanced AI-powered code editor assistant running directly in the terminal (TUI).
2
+ Your goal is to help the user with coding tasks, debugging, file management, and system operations.
3
+
4
+ You have access to the following powerful tools:
5
+ - **File System**: Read, write, list, and search files via MCP. Use these to explore the project structure and understand the codebase.
6
+ - **Shell**: Execute terminal commands. Use this to run builds, tests, or system utilities.
7
+ - **Web Search**: Search the web (DuckDuckGo) for documentation, error solutions, or latest library usage.
8
+ - **Fetch Content**: Retrieve content from URLs to get detailed documentation or examples.
9
+
10
+ ALWAYS create a .agent folder id not there and log all your actions and decisions making md files inside it. before taking any action read the
11
+ past operations from the log files to understand what has been done already. try to have main central_log.md file which links to other log files so instead of searching
12
+ through multiple files you can just read the central log file to get context. and after every operation update the central log file with new operation details.
13
+ but if the user's query is trivial, you can skip logging for that operation logging mus be related to project operations only not for trivial things like creating hello world file.
14
+
15
+ **CRITICAL OPERATIONAL RULES:**
16
+
17
+ 1. **Ask Before Implementing**: NEVER start writing code or creating files without first explaining your plan and getting explicit user confirmation.
18
+ - *Exception*: If the user's request is trivial (e.g., "create a hello world file") or explicitly instructs you to proceed immediately.
19
+ - *Standard Flow*: Analyze -> Propose Plan -> Wait for "Yes" -> Execute.
20
+
21
+ 2. **Explore First**: When asked to work on a project, first list files or read relevant files to understand the context. Do not guess file paths or contents.
22
+
23
+ 3. **Be Concise**: You are in a terminal environment. Keep responses short, clear, and to the point. Avoid excessive Markdown headers or verbosity.
24
+
25
+ 4. **Safety First**: Be extremely careful with destructive operations (deleting files, overwriting files, running unknown scripts). Always warn the user about potential side effects.
26
+
27
+ 5. **Tool Usage**:
28
+ - File system (read, write, list, search files via MCP)
29
+ - Shell commands (run terminal commands)
30
+ - Web search (search DuckDuckGo for docs, references, solutions)
31
+ - Fetch content (grab webpage content for context)
32
+ 6. **Error Handling**: If a tool fails (e.g., file not found, command error), report the error clearly and suggest next steps instead of guessing.
33
+
34
+ 7. **Clarify Ambiguities**:
35
+ - If the user's request is ambiguous or lacks detail, ask clarifying questions before proceeding.
36
+
37
+ Your ultimate goal is to assist the user effectively while ensuring safety, clarity, and precision in all operations.
38
+ If you are unsure about a request, ask clarifying questions. Your primary objective is to be a reliable, safe, and intelligent coding partner.`;
@@ -0,0 +1,71 @@
1
+ import { google } from "@ai-sdk/google";
2
+ import { createOpenAI } from "@ai-sdk/openai";
3
+ import { groq } from "@ai-sdk/groq";
4
+ import { anthropic } from "@ai-sdk/anthropic";
5
+ import { xai } from "@ai-sdk/xai";
6
+ import { loadConfig, getApiKey, type ProviderName } from "./config.js";
7
+ import { createGeminiProvider } from 'ai-sdk-provider-gemini-cli';
8
+
9
+ const OPENAI_COMPATIBLE_URLS: Record<string, string> = {
10
+ deepseek: "https://api.deepseek.com",
11
+ qwen: "https://dashscope.aliyuncs.com/compatible-mode/v1",
12
+ kimi: "https://api.moonshot.cn/v1",
13
+ minimax: "https://api.minimax.chat/v1",
14
+ openai: "https://api.openai.com/v1",
15
+ };
16
+
17
+ const gemini = createGeminiProvider({
18
+ authType: 'oauth-personal',
19
+ });
20
+
21
+ export function getModel(providerName?: ProviderName, modelName?: string) {
22
+ const config = loadConfig();
23
+ const provider = providerName || config.provider;
24
+ const model = modelName || config.model;
25
+ const apiKey = getApiKey(provider);
26
+
27
+ switch (provider) {
28
+ case "google":
29
+ return google(model);
30
+
31
+ case "anthropic":
32
+ return anthropic(model);
33
+
34
+ case "groq":
35
+ return groq(model);
36
+
37
+ case "xai":
38
+ return xai(model);
39
+
40
+ case "gemini":
41
+ return gemini(model);
42
+
43
+ case "openai":
44
+ case "deepseek":
45
+ case "qwen":
46
+ case "kimi":
47
+ case "minimax": {
48
+ const openaiClient = createOpenAI({
49
+ baseURL: OPENAI_COMPATIBLE_URLS[provider],
50
+ apiKey: apiKey,
51
+ });
52
+ return openaiClient(model);
53
+ }
54
+
55
+ default:
56
+ throw new Error(`Unknown provider: ${provider}`);
57
+ }
58
+ }
59
+
60
+ export const PROVIDER_MODELS: Record<ProviderName, string[]> = {
61
+ google: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash"],
62
+ openai: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini"],
63
+ anthropic: ["claude-sonnet-4-20250514", "claude-3-5-haiku-latest", "claude-3-opus-latest"],
64
+ groq: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
65
+ xai: ["grok-2", "grok-2-vision", "grok-beta"],
66
+ deepseek: ["deepseek-chat", "deepseek-reasoner"],
67
+ qwen: ["qwen-turbo", "qwen-plus", "qwen-max"],
68
+ kimi: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"],
69
+ minimax: ["abab6.5-chat", "abab5.5-chat"],
70
+ gemini: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-3-pro-preview","gemini-3-flash-preview"],
71
+ };
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { tool } from "ai";
3
+
4
+ export const shellTool = tool({
5
+ description: "Execute a shell command in the terminal",
6
+ inputSchema: z.object({
7
+ command: z.string().describe("The shell command to execute"),
8
+ cwd: z.string().optional().describe("Working directory for the command"),
9
+ }),
10
+ execute: async ({ command, cwd }) => {
11
+ try {
12
+ const proc = Bun.spawn(["sh", "-c", command], {
13
+ cwd: cwd || process.cwd(),
14
+ stdout: "pipe",
15
+ stderr: "pipe",
16
+ });
17
+
18
+ const stdout = await new Response(proc.stdout).text();
19
+ const stderr = await new Response(proc.stderr).text();
20
+ const exitCode = await proc.exited;
21
+
22
+ if (exitCode !== 0) {
23
+ return `Command failed (exit code ${exitCode}):\n${stderr || stdout}`;
24
+ }
25
+
26
+ return stdout || "(no output)";
27
+ } catch (error: any) {
28
+ return `Error: ${error.message}`;
29
+ }
30
+ },
31
+ });
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ type AutocompleteProps = {
5
+ items: string[];
6
+ selectedIndex: number;
7
+ };
8
+
9
+ export const Autocomplete: React.FC<AutocompleteProps> = ({ items, selectedIndex }) => {
10
+ if (items.length === 0) return null;
11
+
12
+ return (
13
+ <Box flexDirection="column" borderStyle="round" borderColor="blue" paddingX={1}>
14
+ {items.map((item, index) => (
15
+ <Text key={item} color={index === selectedIndex ? "green" : "gray"}>
16
+ {index === selectedIndex ? "> " : " "}
17
+ {item}
18
+ </Text>
19
+ ))}
20
+ </Box>
21
+ );
22
+ };
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ type HeaderProps = {
5
+ provider: string;
6
+ model: string;
7
+ };
8
+
9
+ export const Header: React.FC<HeaderProps> = ({ provider, model }) => {
10
+ const cwd = process.cwd();
11
+ const dirName = cwd.split("/").pop() || cwd;
12
+
13
+ return (
14
+ <Box flexDirection="column" paddingX={1} marginBottom={1}>
15
+ <Box>
16
+ <Text color="cyan" bold>AXE</Text>
17
+ <Text dimColor> • </Text>
18
+ <Text color="yellow">{dirName}</Text>
19
+ <Text dimColor> • </Text>
20
+ <Text color="magenta">{provider}/{model}</Text>
21
+ </Box>
22
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
23
+ </Box>
24
+ );
25
+ };
@@ -0,0 +1,121 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import TextInput from "ink-text-input";
4
+ import Spinner from "ink-spinner";
5
+ import { Autocomplete } from "./autocomplete";
6
+ import { getAllFiles } from "../lib/filesystem";
7
+
8
+ type InputAreaProps = {
9
+ onSubmit: (value: string) => void;
10
+ isLoading: boolean;
11
+ };
12
+
13
+ export const InputArea: React.FC<InputAreaProps> = React.memo(({ onSubmit, isLoading }) => {
14
+ const [query, setQuery] = useState("");
15
+ const [showSuggestions, setShowSuggestions] = useState(false);
16
+ const [filteredFiles, setFilteredFiles] = useState<string[]>([]);
17
+ const [selectedIndex, setSelectedIndex] = useState(0);
18
+ const [allFiles, setAllFiles] = useState<string[]>([]);
19
+
20
+ useEffect(() => {
21
+ setAllFiles(getAllFiles());
22
+ }, []);
23
+
24
+ const handleChange = (value: string) => {
25
+ setQuery(value);
26
+
27
+ const lastWord = value.split(/\s+/).pop() || "";
28
+ if (lastWord.startsWith("@")) {
29
+ const searchTerm = lastWord.slice(1).toLowerCase();
30
+ const matches = allFiles
31
+ .filter((f) => f.toLowerCase().includes(searchTerm))
32
+ .slice(0, 5);
33
+
34
+ setFilteredFiles(matches);
35
+ setShowSuggestions(matches.length > 0);
36
+ setSelectedIndex(0);
37
+ } else {
38
+ setShowSuggestions(false);
39
+ }
40
+ };
41
+
42
+ useInput((input, key) => {
43
+ if (key.upArrow) {
44
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
45
+ }
46
+
47
+ if (key.downArrow) {
48
+ setSelectedIndex((prev) => Math.min(filteredFiles.length - 1, prev + 1));
49
+ }
50
+
51
+ if (key.return || key.tab) {
52
+ if (filteredFiles[selectedIndex]) {
53
+ const parts = query.split(/\s+/);
54
+ parts.pop();
55
+ const newQuery = [...parts, `@${filteredFiles[selectedIndex]} `].join(" ");
56
+ setQuery(newQuery);
57
+ setShowSuggestions(false);
58
+ }
59
+ }
60
+
61
+ if (key.escape) {
62
+ setShowSuggestions(false);
63
+ }
64
+ }, { isActive: showSuggestions });
65
+
66
+ const handleSubmit = (value: string) => {
67
+ if (showSuggestions) {
68
+ return;
69
+ }
70
+
71
+ if (isLoading || !value.trim()) return;
72
+ onSubmit(value);
73
+ setQuery("");
74
+ };
75
+
76
+ return (
77
+ <Box flexDirection="column">
78
+ {showSuggestions && (
79
+ <Autocomplete items={filteredFiles} selectedIndex={selectedIndex} />
80
+ )}
81
+
82
+ {/* Input Box */}
83
+ <Box
84
+ borderStyle="round"
85
+ borderColor={isLoading ? "yellow" : "green"}
86
+ paddingX={1}
87
+ >
88
+ {isLoading ? (
89
+ <Box>
90
+ <Text color="yellow">
91
+ <Spinner type="dots" />
92
+ </Text>
93
+ <Text color="yellow"> Thinking...</Text>
94
+ </Box>
95
+ ) : (
96
+ <Box>
97
+ <Text color="green" bold>❯ </Text>
98
+ <TextInput
99
+ value={query}
100
+ onChange={handleChange}
101
+ onSubmit={handleSubmit}
102
+ placeholder="Ask anything... (@ to reference files)"
103
+ />
104
+ </Box>
105
+ )}
106
+ </Box>
107
+
108
+ {/* Commands hint */}
109
+ <Box paddingX={1} marginTop={1}>
110
+ <Text dimColor>
111
+ <Text color="gray">/new</Text> <Text dimColor>•</Text>{" "}
112
+ <Text color="gray">/clear</Text> <Text dimColor>•</Text>{" "}
113
+ <Text color="gray">/history</Text> <Text dimColor>•</Text>{" "}
114
+ <Text color="gray">/provider</Text> <Text dimColor>•</Text>{" "}
115
+ <Text color="gray">/model</Text> <Text dimColor>•</Text>{" "}
116
+ <Text color="gray">/agent</Text>
117
+ </Text>
118
+ </Box>
119
+ </Box>
120
+ );
121
+ });
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ type LayoutProps = {
5
+ header?: React.ReactNode;
6
+ footer?: React.ReactNode;
7
+ children: React.ReactNode;
8
+ };
9
+
10
+ export const Layout: React.FC<LayoutProps> = ({ header, footer, children }) => {
11
+ return (
12
+ <Box flexDirection="column">
13
+ {header && (
14
+ <Box borderStyle="single" borderColor="blue" paddingX={1}>
15
+ {header}
16
+ </Box>
17
+ )}
18
+
19
+ <Box flexDirection="column" paddingX={1}>
20
+ {children}
21
+ </Box>
22
+
23
+ <Box flexDirection="column" paddingX={1}>
24
+ {footer}
25
+ </Box>
26
+ </Box>
27
+ );
28
+ };
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import { Text, Box } from "ink";
3
+
4
+ type MessageProps = {
5
+ role: "user" | "assistant" | "system";
6
+ content: string;
7
+ thinking?: string;
8
+ };
9
+
10
+ export const MessageComponent: React.FC<MessageProps> = React.memo(({ role, content, thinking }) => {
11
+ const isUser = role === "user";
12
+
13
+ return (
14
+ <Box flexDirection="column" marginBottom={1}>
15
+ {/* Message Header */}
16
+ <Box>
17
+ <Text color={isUser ? "green" : "cyan"} bold>
18
+ {isUser ? "> You" : "| AXE"}
19
+ </Text>
20
+ </Box>
21
+
22
+ {/* Message Content */}
23
+ <Box paddingLeft={3} flexDirection="column">
24
+ {/* Thinking indicator */}
25
+ {thinking && (
26
+ <Box marginBottom={1}>
27
+ <Text color="yellow" dimColor>
28
+ 💭 {thinking}
29
+ </Text>
30
+ </Box>
31
+ )}
32
+
33
+ {/* Main content */}
34
+ <Text wrap="wrap">{content}</Text>
35
+ </Box>
36
+ </Box>
37
+ );
38
+ });