activo 0.2.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.
Files changed (86) hide show
  1. package/FINAL_SIMPLIFIED_SPEC.md +456 -0
  2. package/README.md +62 -0
  3. package/TODO.md +193 -0
  4. package/dist/cli/banner.d.ts +3 -0
  5. package/dist/cli/banner.d.ts.map +1 -0
  6. package/dist/cli/banner.js +30 -0
  7. package/dist/cli/banner.js.map +1 -0
  8. package/dist/cli/headless.d.ts +3 -0
  9. package/dist/cli/headless.d.ts.map +1 -0
  10. package/dist/cli/headless.js +34 -0
  11. package/dist/cli/headless.js.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +42 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/core/agent.d.ts +23 -0
  17. package/dist/core/agent.d.ts.map +1 -0
  18. package/dist/core/agent.js +171 -0
  19. package/dist/core/agent.js.map +1 -0
  20. package/dist/core/config.d.ts +24 -0
  21. package/dist/core/config.d.ts.map +1 -0
  22. package/dist/core/config.js +66 -0
  23. package/dist/core/config.js.map +1 -0
  24. package/dist/core/llm/ollama.d.ts +30 -0
  25. package/dist/core/llm/ollama.d.ts.map +1 -0
  26. package/dist/core/llm/ollama.js +173 -0
  27. package/dist/core/llm/ollama.js.map +1 -0
  28. package/dist/core/mcp/client.d.ts +22 -0
  29. package/dist/core/mcp/client.d.ts.map +1 -0
  30. package/dist/core/mcp/client.js +116 -0
  31. package/dist/core/mcp/client.js.map +1 -0
  32. package/dist/core/tools/builtIn.d.ts +9 -0
  33. package/dist/core/tools/builtIn.d.ts.map +1 -0
  34. package/dist/core/tools/builtIn.js +219 -0
  35. package/dist/core/tools/builtIn.js.map +1 -0
  36. package/dist/core/tools/index.d.ts +13 -0
  37. package/dist/core/tools/index.d.ts.map +1 -0
  38. package/dist/core/tools/index.js +43 -0
  39. package/dist/core/tools/index.js.map +1 -0
  40. package/dist/core/tools/standards.d.ts +6 -0
  41. package/dist/core/tools/standards.d.ts.map +1 -0
  42. package/dist/core/tools/standards.js +215 -0
  43. package/dist/core/tools/standards.js.map +1 -0
  44. package/dist/core/tools/types.d.ts +34 -0
  45. package/dist/core/tools/types.d.ts.map +1 -0
  46. package/dist/core/tools/types.js +2 -0
  47. package/dist/core/tools/types.js.map +1 -0
  48. package/dist/ui/App.d.ts +10 -0
  49. package/dist/ui/App.d.ts.map +1 -0
  50. package/dist/ui/App.js +167 -0
  51. package/dist/ui/App.js.map +1 -0
  52. package/dist/ui/components/InputBox.d.ts +11 -0
  53. package/dist/ui/components/InputBox.d.ts.map +1 -0
  54. package/dist/ui/components/InputBox.js +7 -0
  55. package/dist/ui/components/InputBox.js.map +1 -0
  56. package/dist/ui/components/MessageList.d.ts +17 -0
  57. package/dist/ui/components/MessageList.d.ts.map +1 -0
  58. package/dist/ui/components/MessageList.js +18 -0
  59. package/dist/ui/components/MessageList.js.map +1 -0
  60. package/dist/ui/components/StatusBar.d.ts +9 -0
  61. package/dist/ui/components/StatusBar.d.ts.map +1 -0
  62. package/dist/ui/components/StatusBar.js +6 -0
  63. package/dist/ui/components/StatusBar.js.map +1 -0
  64. package/dist/ui/components/ToolStatus.d.ts +8 -0
  65. package/dist/ui/components/ToolStatus.d.ts.map +1 -0
  66. package/dist/ui/components/ToolStatus.js +7 -0
  67. package/dist/ui/components/ToolStatus.js.map +1 -0
  68. package/package.json +64 -0
  69. package/screenshot.png +0 -0
  70. package/src/cli/banner.ts +34 -0
  71. package/src/cli/headless.ts +37 -0
  72. package/src/cli/index.ts +53 -0
  73. package/src/core/agent.ts +235 -0
  74. package/src/core/config.ts +98 -0
  75. package/src/core/llm/ollama.ts +238 -0
  76. package/src/core/mcp/client.ts +143 -0
  77. package/src/core/tools/builtIn.ts +221 -0
  78. package/src/core/tools/index.ts +53 -0
  79. package/src/core/tools/standards.ts +246 -0
  80. package/src/core/tools/types.ts +37 -0
  81. package/src/ui/App.tsx +238 -0
  82. package/src/ui/components/InputBox.tsx +37 -0
  83. package/src/ui/components/MessageList.tsx +80 -0
  84. package/src/ui/components/StatusBar.tsx +36 -0
  85. package/src/ui/components/ToolStatus.tsx +38 -0
  86. package/tsconfig.json +21 -0
@@ -0,0 +1,246 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import pdfParse from "pdf-parse";
4
+ import { Tool, ToolResult } from "./types.js";
5
+ import { OllamaClient } from "../llm/ollama.js";
6
+ import { loadConfig } from "../config.js";
7
+
8
+ // Import PDF Tool
9
+ export const importPdfTool: Tool = {
10
+ name: "import_pdf_standards",
11
+ description: "Import development standards from a PDF file and convert to markdown rules.",
12
+ parameters: {
13
+ type: "object",
14
+ required: ["pdfPath"],
15
+ properties: {
16
+ pdfPath: {
17
+ type: "string",
18
+ description: "Path to the PDF file",
19
+ },
20
+ outputDir: {
21
+ type: "string",
22
+ description: "Output directory (default: .activo/standards)",
23
+ },
24
+ },
25
+ },
26
+ handler: async (args): Promise<ToolResult> => {
27
+ try {
28
+ const pdfPath = path.resolve(args.pdfPath as string);
29
+ const outputDir = path.resolve((args.outputDir as string) || ".activo/standards");
30
+
31
+ if (!fs.existsSync(pdfPath)) {
32
+ return { success: false, content: "", error: `PDF not found: ${pdfPath}` };
33
+ }
34
+
35
+ // Parse PDF
36
+ const dataBuffer = fs.readFileSync(pdfPath);
37
+ const data = await pdfParse(dataBuffer);
38
+
39
+ // Create output directory
40
+ if (!fs.existsSync(outputDir)) {
41
+ fs.mkdirSync(outputDir, { recursive: true });
42
+ }
43
+
44
+ // Split into chunks (simple approach: by paragraphs)
45
+ const chunks = splitIntoChunks(data.text, 3000);
46
+ const filename = path.basename(pdfPath, ".pdf");
47
+ const extractionDate = new Date().toISOString().split("T")[0];
48
+
49
+ // Save chunks as markdown
50
+ const results: string[] = [];
51
+ for (let i = 0; i < chunks.length; i++) {
52
+ const chunkFilename = `${String(i + 1).padStart(2, "0")}_${sanitize(filename)}.md`;
53
+ const chunkPath = path.join(outputDir, chunkFilename);
54
+
55
+ let md = `# ${filename} - Part ${i + 1}\n\n`;
56
+ md += `> Source: ${path.basename(pdfPath)}\n`;
57
+ md += `> Extracted: ${extractionDate}\n`;
58
+ md += `> Pages: ${data.numpages}\n\n`;
59
+ md += `---\n\n`;
60
+ md += chunks[i];
61
+ md += `\n\n---\n`;
62
+ md += `[Edit this file to add structured rules]\n`;
63
+
64
+ fs.writeFileSync(chunkPath, md);
65
+ results.push(chunkFilename);
66
+ }
67
+
68
+ // Create index
69
+ const indexPath = path.join(outputDir, "_index.md");
70
+ let indexMd = `# Development Standards Index\n\n`;
71
+ indexMd += `> Source: ${path.basename(pdfPath)}\n`;
72
+ indexMd += `> Extracted: ${extractionDate}\n`;
73
+ indexMd += `> Files: ${results.length}\n\n`;
74
+ indexMd += `## Files\n\n`;
75
+ for (const r of results) {
76
+ indexMd += `- [${r}](./${r})\n`;
77
+ }
78
+ fs.writeFileSync(indexPath, indexMd);
79
+
80
+ return {
81
+ success: true,
82
+ content: `Imported ${results.length} files to ${outputDir}\n\nFiles:\n${results.join("\n")}\n\nNext: Edit the files to add structured rules with format:\n## RULE-001: Title\n- 심각도: error|warning|info\n- 규칙: description`,
83
+ };
84
+ } catch (error) {
85
+ return { success: false, content: "", error: String(error) };
86
+ }
87
+ },
88
+ };
89
+
90
+ // List Standards Tool
91
+ export const listStandardsTool: Tool = {
92
+ name: "list_standards",
93
+ description: "List all loaded development standards and rules.",
94
+ parameters: {
95
+ type: "object",
96
+ properties: {
97
+ directory: {
98
+ type: "string",
99
+ description: "Standards directory (default: .activo/standards)",
100
+ },
101
+ },
102
+ },
103
+ handler: async (args): Promise<ToolResult> => {
104
+ try {
105
+ const dir = path.resolve((args.directory as string) || ".activo/standards");
106
+
107
+ if (!fs.existsSync(dir)) {
108
+ return { success: true, content: "No standards directory found. Import a PDF first." };
109
+ }
110
+
111
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md") && f !== "_index.md");
112
+ if (files.length === 0) {
113
+ return { success: true, content: "No standard files found." };
114
+ }
115
+
116
+ let totalRules = 0;
117
+ const results: string[] = [];
118
+
119
+ for (const file of files) {
120
+ const content = fs.readFileSync(path.join(dir, file), "utf-8");
121
+ const rules = content.match(/^## [A-Z]+-\d+/gm) || [];
122
+ totalRules += rules.length;
123
+ results.push(`📄 ${file}: ${rules.length} rules`);
124
+ }
125
+
126
+ return {
127
+ success: true,
128
+ content: `Standards Directory: ${dir}\n\n${results.join("\n")}\n\nTotal: ${files.length} files, ${totalRules} rules`,
129
+ };
130
+ } catch (error) {
131
+ return { success: false, content: "", error: String(error) };
132
+ }
133
+ },
134
+ };
135
+
136
+ // Check Code Quality Tool
137
+ export const checkQualityTool: Tool = {
138
+ name: "check_code_quality",
139
+ description: "Check code against loaded development standards.",
140
+ parameters: {
141
+ type: "object",
142
+ required: ["filepath"],
143
+ properties: {
144
+ filepath: {
145
+ type: "string",
146
+ description: "File or directory to check",
147
+ },
148
+ standardsDir: {
149
+ type: "string",
150
+ description: "Standards directory (default: .activo/standards)",
151
+ },
152
+ },
153
+ },
154
+ handler: async (args): Promise<ToolResult> => {
155
+ try {
156
+ const filepath = path.resolve(args.filepath as string);
157
+ const standardsDir = path.resolve((args.standardsDir as string) || ".activo/standards");
158
+
159
+ if (!fs.existsSync(filepath)) {
160
+ return { success: false, content: "", error: `Path not found: ${filepath}` };
161
+ }
162
+
163
+ // Load standards
164
+ let standards = "";
165
+ if (fs.existsSync(standardsDir)) {
166
+ const files = fs.readdirSync(standardsDir).filter((f) => f.endsWith(".md") && f !== "_index.md");
167
+ for (const file of files) {
168
+ standards += fs.readFileSync(path.join(standardsDir, file), "utf-8") + "\n\n";
169
+ }
170
+ }
171
+
172
+ // Get code
173
+ let code = "";
174
+ const stat = fs.statSync(filepath);
175
+ if (stat.isFile()) {
176
+ code = fs.readFileSync(filepath, "utf-8");
177
+ } else {
178
+ return { success: false, content: "", error: "Directory check not yet supported. Specify a file." };
179
+ }
180
+
181
+ // Build analysis prompt
182
+ const prompt = buildAnalysisPrompt(code, filepath, standards);
183
+
184
+ // Call Ollama
185
+ const config = loadConfig();
186
+ const client = new OllamaClient(config.ollama);
187
+
188
+ const response = await client.chat([{ role: "user", content: prompt }]);
189
+
190
+ return { success: true, content: response.content };
191
+ } catch (error) {
192
+ return { success: false, content: "", error: String(error) };
193
+ }
194
+ },
195
+ };
196
+
197
+ // Helper functions
198
+ function splitIntoChunks(text: string, maxSize: number): string[] {
199
+ const chunks: string[] = [];
200
+ const paragraphs = text.split(/\n\n+/);
201
+ let current = "";
202
+
203
+ for (const para of paragraphs) {
204
+ if (current.length + para.length > maxSize && current.length > 0) {
205
+ chunks.push(current.trim());
206
+ current = "";
207
+ }
208
+ current += para + "\n\n";
209
+ }
210
+
211
+ if (current.trim()) {
212
+ chunks.push(current.trim());
213
+ }
214
+
215
+ return chunks.length > 0 ? chunks : [text];
216
+ }
217
+
218
+ function sanitize(name: string): string {
219
+ return name.replace(/[^a-zA-Z0-9가-힣\s-]/g, "").replace(/\s+/g, "_").slice(0, 50);
220
+ }
221
+
222
+ function buildAnalysisPrompt(code: string, filepath: string, standards: string): string {
223
+ const ext = path.extname(filepath);
224
+ const lang = { ".ts": "typescript", ".js": "javascript", ".java": "java", ".py": "python" }[ext] || "text";
225
+
226
+ let prompt = `당신은 코드 품질 전문가입니다. 아래 코드를 분석하세요.\n\n`;
227
+
228
+ if (standards) {
229
+ prompt += `[개발 표준 규칙]\n${standards.slice(0, 4000)}\n\n`;
230
+ }
231
+
232
+ prompt += `[분석 대상 코드]\n파일: ${filepath}\n\`\`\`${lang}\n${code.slice(0, 8000)}\n\`\`\`\n\n`;
233
+ prompt += `[점검 요청]\n`;
234
+ prompt += `1. 규칙 위반 사항 (있다면)\n`;
235
+ prompt += `2. 개선 제안\n`;
236
+ prompt += `3. 전반적인 코드 품질 평가\n`;
237
+
238
+ return prompt;
239
+ }
240
+
241
+ // All standards tools
242
+ export const standardsTools: Tool[] = [
243
+ importPdfTool,
244
+ listStandardsTool,
245
+ checkQualityTool,
246
+ ];
@@ -0,0 +1,37 @@
1
+ export interface Tool {
2
+ name: string;
3
+ description: string;
4
+ parameters: {
5
+ type: "object";
6
+ required?: string[];
7
+ properties: Record<string, ToolParameter>;
8
+ };
9
+ handler: (args: Record<string, unknown>) => Promise<ToolResult>;
10
+ }
11
+
12
+ export interface ToolParameter {
13
+ type: string;
14
+ description: string;
15
+ enum?: string[];
16
+ default?: unknown;
17
+ }
18
+
19
+ export interface ToolCall {
20
+ id: string;
21
+ name: string;
22
+ arguments: Record<string, unknown>;
23
+ }
24
+
25
+ export interface ToolResult {
26
+ success: boolean;
27
+ content: string;
28
+ error?: string;
29
+ }
30
+
31
+ export interface ToolEvent {
32
+ type: "tool_use";
33
+ tool: string;
34
+ status: "start" | "complete" | "error";
35
+ args?: Record<string, unknown>;
36
+ result?: ToolResult;
37
+ }
package/src/ui/App.tsx ADDED
@@ -0,0 +1,238 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from "react";
2
+ import { Box, Text, useApp, useInput } from "ink";
3
+ import { Config } from "../core/config.js";
4
+ import { OllamaClient, ChatMessage } from "../core/llm/ollama.js";
5
+ import { streamProcessMessage, AgentEvent } from "../core/agent.js";
6
+ import { InputBox } from "./components/InputBox.js";
7
+ import { MessageList } from "./components/MessageList.js";
8
+ import { StatusBar } from "./components/StatusBar.js";
9
+ import { ToolStatus } from "./components/ToolStatus.js";
10
+
11
+ interface AppProps {
12
+ initialPrompt?: string;
13
+ config: Config;
14
+ resume?: boolean;
15
+ }
16
+
17
+ interface Message {
18
+ role: "user" | "assistant";
19
+ content: string;
20
+ toolCalls?: Array<{
21
+ tool: string;
22
+ status: "running" | "complete" | "error";
23
+ result?: string;
24
+ }>;
25
+ }
26
+
27
+ export function App({ initialPrompt, config, resume }: AppProps): React.ReactElement {
28
+ const { exit } = useApp();
29
+ const [messages, setMessages] = useState<Message[]>([]);
30
+ const [input, setInput] = useState("");
31
+ const [isProcessing, setIsProcessing] = useState(false);
32
+ const [currentTool, setCurrentTool] = useState<string | null>(null);
33
+ const [toolStatus, setToolStatus] = useState<"running" | "complete" | "error" | null>(null);
34
+ const [error, setError] = useState<string | null>(null);
35
+ const [client] = useState(() => new OllamaClient(config.ollama));
36
+ const [exitPending, setExitPending] = useState(false);
37
+ const [cancelled, setCancelled] = useState(false);
38
+ const abortControllerRef = useRef<AbortController | null>(null);
39
+
40
+ // Handle Ctrl+C and ESC
41
+ useInput((inputChar, key) => {
42
+ // ESC key to cancel current operation
43
+ if (key.escape && isProcessing) {
44
+ if (abortControllerRef.current) {
45
+ abortControllerRef.current.abort();
46
+ setCancelled(true);
47
+ setIsProcessing(false);
48
+ setCurrentTool(null);
49
+ setToolStatus(null);
50
+ setError("Operation cancelled by user (ESC)");
51
+ }
52
+ return;
53
+ }
54
+
55
+ // Ctrl+C to exit
56
+ if (key.ctrl && inputChar === "c") {
57
+ if (isProcessing && abortControllerRef.current) {
58
+ abortControllerRef.current.abort();
59
+ setCancelled(true);
60
+ setIsProcessing(false);
61
+ setCurrentTool(null);
62
+ setToolStatus(null);
63
+ return;
64
+ }
65
+ if (exitPending) {
66
+ exit();
67
+ } else {
68
+ setExitPending(true);
69
+ setTimeout(() => setExitPending(false), 1000);
70
+ }
71
+ }
72
+ });
73
+
74
+ // Check Ollama connection on mount
75
+ useEffect(() => {
76
+ const checkConnection = async () => {
77
+ const connected = await client.isConnected();
78
+ if (!connected) {
79
+ setError(`Cannot connect to Ollama at ${config.ollama.baseUrl}`);
80
+ }
81
+ };
82
+ checkConnection();
83
+ }, [client, config.ollama.baseUrl]);
84
+
85
+ // Process initial prompt
86
+ useEffect(() => {
87
+ if (initialPrompt) {
88
+ handleSubmit(initialPrompt);
89
+ }
90
+ }, [initialPrompt]);
91
+
92
+ const handleSubmit = useCallback(async (text: string) => {
93
+ if (!text.trim() || isProcessing) return;
94
+
95
+ setInput("");
96
+ setIsProcessing(true);
97
+ setError(null);
98
+ setCancelled(false);
99
+
100
+ // Create AbortController for this request
101
+ const abortController = new AbortController();
102
+ abortControllerRef.current = abortController;
103
+
104
+ // Add user message
105
+ const userMessage: Message = { role: "user", content: text };
106
+ setMessages((prev) => [...prev, userMessage]);
107
+
108
+ // Convert messages to chat format
109
+ const history: ChatMessage[] = messages.map((m) => ({
110
+ role: m.role,
111
+ content: m.content,
112
+ }));
113
+
114
+ // Create assistant message placeholder
115
+ const assistantMessage: Message = { role: "assistant", content: "", toolCalls: [] };
116
+ setMessages((prev) => [...prev, assistantMessage]);
117
+
118
+ try {
119
+ let fullContent = "";
120
+
121
+ for await (const event of streamProcessMessage(text, history, client, config, abortController.signal)) {
122
+ // Check if cancelled
123
+ if (abortController.signal.aborted) {
124
+ break;
125
+ }
126
+ switch (event.type) {
127
+ case "content":
128
+ fullContent += event.content || "";
129
+ setMessages((prev) => {
130
+ const updated = [...prev];
131
+ const last = updated[updated.length - 1];
132
+ if (last.role === "assistant") {
133
+ last.content = fullContent;
134
+ }
135
+ return updated;
136
+ });
137
+ break;
138
+
139
+ case "tool_use":
140
+ setCurrentTool(event.tool || null);
141
+ setToolStatus("running");
142
+ setMessages((prev) => {
143
+ const updated = [...prev];
144
+ const last = updated[updated.length - 1];
145
+ if (last.role === "assistant") {
146
+ last.toolCalls = [
147
+ ...(last.toolCalls || []),
148
+ { tool: event.tool!, status: "running" },
149
+ ];
150
+ }
151
+ return updated;
152
+ });
153
+ break;
154
+
155
+ case "tool_result":
156
+ setToolStatus(event.status as "complete" | "error");
157
+ setMessages((prev) => {
158
+ const updated = [...prev];
159
+ const last = updated[updated.length - 1];
160
+ if (last.role === "assistant" && last.toolCalls) {
161
+ const toolCall = last.toolCalls.find((tc) => tc.tool === event.tool);
162
+ if (toolCall) {
163
+ toolCall.status = event.status as "complete" | "error";
164
+ toolCall.result = event.result?.content || event.result?.error;
165
+ }
166
+ }
167
+ return updated;
168
+ });
169
+ setTimeout(() => {
170
+ setCurrentTool(null);
171
+ setToolStatus(null);
172
+ }, 500);
173
+ break;
174
+
175
+ case "error":
176
+ setError(event.error || "Unknown error");
177
+ break;
178
+
179
+ case "done":
180
+ break;
181
+ }
182
+ }
183
+ } catch (err) {
184
+ if (!abortController.signal.aborted) {
185
+ setError(String(err));
186
+ }
187
+ } finally {
188
+ setIsProcessing(false);
189
+ setCurrentTool(null);
190
+ setToolStatus(null);
191
+ abortControllerRef.current = null;
192
+ }
193
+ }, [messages, client, config, isProcessing]);
194
+
195
+ return (
196
+ <Box flexDirection="column" height="100%">
197
+ {/* Messages */}
198
+ <Box flexDirection="column" flexGrow={1}>
199
+ <MessageList messages={messages} />
200
+ </Box>
201
+
202
+ {/* Tool Status */}
203
+ {currentTool && (
204
+ <ToolStatus tool={currentTool} status={toolStatus || "running"} />
205
+ )}
206
+
207
+ {/* Error */}
208
+ {error && (
209
+ <Box marginY={1}>
210
+ <Text color="red">⚠ {error}</Text>
211
+ </Box>
212
+ )}
213
+
214
+ {/* Exit Warning */}
215
+ {exitPending && (
216
+ <Box marginY={1}>
217
+ <Text color="yellow">Press Ctrl+C again to exit</Text>
218
+ </Box>
219
+ )}
220
+
221
+ {/* Input */}
222
+ <InputBox
223
+ value={input}
224
+ onChange={setInput}
225
+ onSubmit={handleSubmit}
226
+ isProcessing={isProcessing}
227
+ placeholder="Type your message..."
228
+ />
229
+
230
+ {/* Status Bar */}
231
+ <StatusBar
232
+ model={config.ollama.model}
233
+ isProcessing={isProcessing}
234
+ messageCount={messages.length}
235
+ />
236
+ </Box>
237
+ );
238
+ }
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import TextInput from "ink-text-input";
4
+
5
+ interface InputBoxProps {
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ onSubmit: (value: string) => void;
9
+ isProcessing: boolean;
10
+ placeholder?: string;
11
+ }
12
+
13
+ export function InputBox({
14
+ value,
15
+ onChange,
16
+ onSubmit,
17
+ isProcessing,
18
+ placeholder,
19
+ }: InputBoxProps): React.ReactElement {
20
+ return (
21
+ <Box borderStyle="round" borderColor={isProcessing ? "gray" : "cyan"} paddingX={1}>
22
+ <Text color="green" bold>
23
+ {isProcessing ? "⏳" : "❯"}{" "}
24
+ </Text>
25
+ {isProcessing ? (
26
+ <Text color="gray">Processing...</Text>
27
+ ) : (
28
+ <TextInput
29
+ value={value}
30
+ onChange={onChange}
31
+ onSubmit={onSubmit}
32
+ placeholder={placeholder}
33
+ />
34
+ )}
35
+ </Box>
36
+ );
37
+ }
@@ -0,0 +1,80 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ interface ToolCall {
5
+ tool: string;
6
+ status: "running" | "complete" | "error";
7
+ result?: string;
8
+ }
9
+
10
+ interface Message {
11
+ role: "user" | "assistant";
12
+ content: string;
13
+ toolCalls?: ToolCall[];
14
+ }
15
+
16
+ interface MessageListProps {
17
+ messages: Message[];
18
+ }
19
+
20
+ export function MessageList({ messages }: MessageListProps): React.ReactElement {
21
+ if (messages.length === 0) {
22
+ return (
23
+ <Box marginY={1}>
24
+ <Text color="gray">Start a conversation by typing below...</Text>
25
+ </Box>
26
+ );
27
+ }
28
+
29
+ return (
30
+ <Box flexDirection="column">
31
+ {messages.map((message, index) => (
32
+ <MessageItem key={index} message={message} />
33
+ ))}
34
+ </Box>
35
+ );
36
+ }
37
+
38
+ function MessageItem({ message }: { message: Message }): React.ReactElement {
39
+ const isUser = message.role === "user";
40
+
41
+ return (
42
+ <Box flexDirection="column" marginY={1}>
43
+ {/* Role indicator */}
44
+ <Box>
45
+ <Text color={isUser ? "green" : "cyan"} bold>
46
+ {isUser ? "You" : "ACTIVO"}
47
+ </Text>
48
+ </Box>
49
+
50
+ {/* Tool calls */}
51
+ {message.toolCalls && message.toolCalls.length > 0 && (
52
+ <Box flexDirection="column" marginLeft={2} marginY={1}>
53
+ {message.toolCalls.map((tc, idx) => (
54
+ <Box key={idx}>
55
+ <Text color="gray">
56
+ {tc.status === "running" ? "🔄" : tc.status === "complete" ? "✓" : "✗"}{" "}
57
+ </Text>
58
+ <Text color={tc.status === "error" ? "red" : "yellow"}>{tc.tool}</Text>
59
+ {tc.status === "complete" && tc.result && (
60
+ <Text color="gray"> - {truncate(tc.result, 50)}</Text>
61
+ )}
62
+ </Box>
63
+ ))}
64
+ </Box>
65
+ )}
66
+
67
+ {/* Content */}
68
+ {message.content && (
69
+ <Box marginLeft={2}>
70
+ <Text wrap="wrap">{message.content}</Text>
71
+ </Box>
72
+ )}
73
+ </Box>
74
+ );
75
+ }
76
+
77
+ function truncate(text: string, maxLength: number): string {
78
+ if (text.length <= maxLength) return text;
79
+ return text.slice(0, maxLength) + "...";
80
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ interface StatusBarProps {
5
+ model: string;
6
+ isProcessing: boolean;
7
+ messageCount: number;
8
+ }
9
+
10
+ export function StatusBar({
11
+ model,
12
+ isProcessing,
13
+ messageCount,
14
+ }: StatusBarProps): React.ReactElement {
15
+ return (
16
+ <Box justifyContent="space-between" paddingX={1} marginTop={1}>
17
+ <Box>
18
+ <Text color="gray">Model: </Text>
19
+ <Text color="cyan">{model}</Text>
20
+ </Box>
21
+
22
+ <Box>
23
+ <Text color="gray">Messages: </Text>
24
+ <Text color="white">{messageCount}</Text>
25
+ </Box>
26
+
27
+ <Box>
28
+ {isProcessing ? (
29
+ <Text color="yellow">● Processing</Text>
30
+ ) : (
31
+ <Text color="green">● Ready</Text>
32
+ )}
33
+ </Box>
34
+ </Box>
35
+ );
36
+ }