gencode-ai 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.
Files changed (274) hide show
  1. package/.env.example +11 -0
  2. package/CLAUDE.md +70 -0
  3. package/LICENSE +21 -0
  4. package/README.md +117 -0
  5. package/dist/agent/agent.d.ts +84 -0
  6. package/dist/agent/agent.d.ts.map +1 -0
  7. package/dist/agent/agent.js +233 -0
  8. package/dist/agent/agent.js.map +1 -0
  9. package/dist/agent/index.d.ts +6 -0
  10. package/dist/agent/index.d.ts.map +1 -0
  11. package/dist/agent/index.js +6 -0
  12. package/dist/agent/index.js.map +1 -0
  13. package/dist/agent/types.d.ts +47 -0
  14. package/dist/agent/types.d.ts.map +1 -0
  15. package/dist/agent/types.js +5 -0
  16. package/dist/agent/types.js.map +1 -0
  17. package/dist/cli/components/App.d.ts +14 -0
  18. package/dist/cli/components/App.d.ts.map +1 -0
  19. package/dist/cli/components/App.js +395 -0
  20. package/dist/cli/components/App.js.map +1 -0
  21. package/dist/cli/components/CommandSuggestions.d.ts +13 -0
  22. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -0
  23. package/dist/cli/components/CommandSuggestions.js +32 -0
  24. package/dist/cli/components/CommandSuggestions.js.map +1 -0
  25. package/dist/cli/components/Header.d.ts +9 -0
  26. package/dist/cli/components/Header.d.ts.map +1 -0
  27. package/dist/cli/components/Header.js +13 -0
  28. package/dist/cli/components/Header.js.map +1 -0
  29. package/dist/cli/components/Input.d.ts +13 -0
  30. package/dist/cli/components/Input.d.ts.map +1 -0
  31. package/dist/cli/components/Input.js +27 -0
  32. package/dist/cli/components/Input.js.map +1 -0
  33. package/dist/cli/components/Logo.d.ts +2 -0
  34. package/dist/cli/components/Logo.d.ts.map +1 -0
  35. package/dist/cli/components/Logo.js +8 -0
  36. package/dist/cli/components/Logo.js.map +1 -0
  37. package/dist/cli/components/Messages.d.ts +37 -0
  38. package/dist/cli/components/Messages.d.ts.map +1 -0
  39. package/dist/cli/components/Messages.js +106 -0
  40. package/dist/cli/components/Messages.js.map +1 -0
  41. package/dist/cli/components/ModelSelector.d.ts +13 -0
  42. package/dist/cli/components/ModelSelector.d.ts.map +1 -0
  43. package/dist/cli/components/ModelSelector.js +72 -0
  44. package/dist/cli/components/ModelSelector.js.map +1 -0
  45. package/dist/cli/components/Spinner.d.ts +12 -0
  46. package/dist/cli/components/Spinner.d.ts.map +1 -0
  47. package/dist/cli/components/Spinner.js +45 -0
  48. package/dist/cli/components/Spinner.js.map +1 -0
  49. package/dist/cli/components/index.d.ts +12 -0
  50. package/dist/cli/components/index.d.ts.map +1 -0
  51. package/dist/cli/components/index.js +12 -0
  52. package/dist/cli/components/index.js.map +1 -0
  53. package/dist/cli/components/theme.d.ts +31 -0
  54. package/dist/cli/components/theme.d.ts.map +1 -0
  55. package/dist/cli/components/theme.js +36 -0
  56. package/dist/cli/components/theme.js.map +1 -0
  57. package/dist/cli/index-legacy.d.ts +7 -0
  58. package/dist/cli/index-legacy.d.ts.map +1 -0
  59. package/dist/cli/index-legacy.js +431 -0
  60. package/dist/cli/index-legacy.js.map +1 -0
  61. package/dist/cli/index.d.ts +7 -0
  62. package/dist/cli/index.d.ts.map +1 -0
  63. package/dist/cli/index.js +116 -0
  64. package/dist/cli/index.js.map +1 -0
  65. package/dist/cli/ink-cli.d.ts +7 -0
  66. package/dist/cli/ink-cli.d.ts.map +1 -0
  67. package/dist/cli/ink-cli.js +105 -0
  68. package/dist/cli/ink-cli.js.map +1 -0
  69. package/dist/cli/session-picker.d.ts +16 -0
  70. package/dist/cli/session-picker.d.ts.map +1 -0
  71. package/dist/cli/session-picker.js +280 -0
  72. package/dist/cli/session-picker.js.map +1 -0
  73. package/dist/cli/ui.d.ts +61 -0
  74. package/dist/cli/ui.d.ts.map +1 -0
  75. package/dist/cli/ui.js +364 -0
  76. package/dist/cli/ui.js.map +1 -0
  77. package/dist/config/index.d.ts +7 -0
  78. package/dist/config/index.d.ts.map +1 -0
  79. package/dist/config/index.js +6 -0
  80. package/dist/config/index.js.map +1 -0
  81. package/dist/config/manager.d.ts +31 -0
  82. package/dist/config/manager.d.ts.map +1 -0
  83. package/dist/config/manager.js +65 -0
  84. package/dist/config/manager.js.map +1 -0
  85. package/dist/config/types.d.ts +22 -0
  86. package/dist/config/types.d.ts.map +1 -0
  87. package/dist/config/types.js +6 -0
  88. package/dist/config/types.js.map +1 -0
  89. package/dist/index.d.ts +12 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +21 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/memory/index.d.ts +10 -0
  94. package/dist/memory/index.d.ts.map +1 -0
  95. package/dist/memory/index.js +9 -0
  96. package/dist/memory/index.js.map +1 -0
  97. package/dist/memory/init.d.ts +20 -0
  98. package/dist/memory/init.d.ts.map +1 -0
  99. package/dist/memory/init.js +332 -0
  100. package/dist/memory/init.js.map +1 -0
  101. package/dist/memory/manager.d.ts +85 -0
  102. package/dist/memory/manager.d.ts.map +1 -0
  103. package/dist/memory/manager.js +234 -0
  104. package/dist/memory/manager.js.map +1 -0
  105. package/dist/memory/types.d.ts +74 -0
  106. package/dist/memory/types.d.ts.map +1 -0
  107. package/dist/memory/types.js +6 -0
  108. package/dist/memory/types.js.map +1 -0
  109. package/dist/permissions/index.d.ts +7 -0
  110. package/dist/permissions/index.d.ts.map +1 -0
  111. package/dist/permissions/index.js +6 -0
  112. package/dist/permissions/index.js.map +1 -0
  113. package/dist/permissions/manager.d.ts +32 -0
  114. package/dist/permissions/manager.d.ts.map +1 -0
  115. package/dist/permissions/manager.js +79 -0
  116. package/dist/permissions/manager.js.map +1 -0
  117. package/dist/permissions/types.d.ts +14 -0
  118. package/dist/permissions/types.d.ts.map +1 -0
  119. package/dist/permissions/types.js +17 -0
  120. package/dist/permissions/types.js.map +1 -0
  121. package/dist/providers/anthropic.d.ts +20 -0
  122. package/dist/providers/anthropic.d.ts.map +1 -0
  123. package/dist/providers/anthropic.js +185 -0
  124. package/dist/providers/anthropic.js.map +1 -0
  125. package/dist/providers/gemini.d.ts +21 -0
  126. package/dist/providers/gemini.d.ts.map +1 -0
  127. package/dist/providers/gemini.js +241 -0
  128. package/dist/providers/gemini.js.map +1 -0
  129. package/dist/providers/index.d.ts +34 -0
  130. package/dist/providers/index.d.ts.map +1 -0
  131. package/dist/providers/index.js +72 -0
  132. package/dist/providers/index.js.map +1 -0
  133. package/dist/providers/openai.d.ts +19 -0
  134. package/dist/providers/openai.d.ts.map +1 -0
  135. package/dist/providers/openai.js +221 -0
  136. package/dist/providers/openai.js.map +1 -0
  137. package/dist/providers/types.d.ts +125 -0
  138. package/dist/providers/types.d.ts.map +1 -0
  139. package/dist/providers/types.js +6 -0
  140. package/dist/providers/types.js.map +1 -0
  141. package/dist/session/index.d.ts +6 -0
  142. package/dist/session/index.d.ts.map +1 -0
  143. package/dist/session/index.js +6 -0
  144. package/dist/session/index.js.map +1 -0
  145. package/dist/session/manager.d.ts +101 -0
  146. package/dist/session/manager.d.ts.map +1 -0
  147. package/dist/session/manager.js +295 -0
  148. package/dist/session/manager.js.map +1 -0
  149. package/dist/session/types.d.ts +39 -0
  150. package/dist/session/types.d.ts.map +1 -0
  151. package/dist/session/types.js +10 -0
  152. package/dist/session/types.js.map +1 -0
  153. package/dist/tools/builtin/bash.d.ts +7 -0
  154. package/dist/tools/builtin/bash.d.ts.map +1 -0
  155. package/dist/tools/builtin/bash.js +80 -0
  156. package/dist/tools/builtin/bash.js.map +1 -0
  157. package/dist/tools/builtin/edit.d.ts +7 -0
  158. package/dist/tools/builtin/edit.d.ts.map +1 -0
  159. package/dist/tools/builtin/edit.js +32 -0
  160. package/dist/tools/builtin/edit.js.map +1 -0
  161. package/dist/tools/builtin/glob.d.ts +7 -0
  162. package/dist/tools/builtin/glob.d.ts.map +1 -0
  163. package/dist/tools/builtin/glob.js +36 -0
  164. package/dist/tools/builtin/glob.js.map +1 -0
  165. package/dist/tools/builtin/grep.d.ts +7 -0
  166. package/dist/tools/builtin/grep.d.ts.map +1 -0
  167. package/dist/tools/builtin/grep.js +59 -0
  168. package/dist/tools/builtin/grep.js.map +1 -0
  169. package/dist/tools/builtin/read.d.ts +7 -0
  170. package/dist/tools/builtin/read.d.ts.map +1 -0
  171. package/dist/tools/builtin/read.js +29 -0
  172. package/dist/tools/builtin/read.js.map +1 -0
  173. package/dist/tools/builtin/write.d.ts +7 -0
  174. package/dist/tools/builtin/write.d.ts.map +1 -0
  175. package/dist/tools/builtin/write.js +24 -0
  176. package/dist/tools/builtin/write.js.map +1 -0
  177. package/dist/tools/index.d.ts +38 -0
  178. package/dist/tools/index.d.ts.map +1 -0
  179. package/dist/tools/index.js +32 -0
  180. package/dist/tools/index.js.map +1 -0
  181. package/dist/tools/registry.d.ts +22 -0
  182. package/dist/tools/registry.d.ts.map +1 -0
  183. package/dist/tools/registry.js +71 -0
  184. package/dist/tools/registry.js.map +1 -0
  185. package/dist/tools/types.d.ts +62 -0
  186. package/dist/tools/types.d.ts.map +1 -0
  187. package/dist/tools/types.js +126 -0
  188. package/dist/tools/types.js.map +1 -0
  189. package/docs/README.md +16 -0
  190. package/docs/proposals/0001-web-fetch-tool.md +293 -0
  191. package/docs/proposals/0002-web-search-tool.md +306 -0
  192. package/docs/proposals/0003-task-subagents.md +333 -0
  193. package/docs/proposals/0004-plan-mode.md +338 -0
  194. package/docs/proposals/0005-todo-system.md +299 -0
  195. package/docs/proposals/0006-memory-system.md +539 -0
  196. package/docs/proposals/0007-context-management.md +429 -0
  197. package/docs/proposals/0008-checkpointing.md +327 -0
  198. package/docs/proposals/0009-hooks-system.md +343 -0
  199. package/docs/proposals/0010-mcp-integration.md +382 -0
  200. package/docs/proposals/0011-custom-commands.md +374 -0
  201. package/docs/proposals/0012-ask-user-question.md +317 -0
  202. package/docs/proposals/0013-multi-edit-tool.md +345 -0
  203. package/docs/proposals/0014-lsp-tool.md +478 -0
  204. package/docs/proposals/0015-ls-tool.md +407 -0
  205. package/docs/proposals/0016-kill-shell-tool.md +455 -0
  206. package/docs/proposals/0017-background-tasks.md +489 -0
  207. package/docs/proposals/0018-parallel-tool-execution.md +415 -0
  208. package/docs/proposals/0019-session-enhancements.md +462 -0
  209. package/docs/proposals/0020-session-summarization.md +447 -0
  210. package/docs/proposals/0021-skills-system.md +409 -0
  211. package/docs/proposals/0022-plugin-system.md +467 -0
  212. package/docs/proposals/0023-permission-enhancements.md +470 -0
  213. package/docs/proposals/0024-keyboard-shortcuts.md +443 -0
  214. package/docs/proposals/0025-cost-tracking.md +447 -0
  215. package/docs/proposals/0026-git-integration.md +475 -0
  216. package/docs/proposals/0027-enhanced-read-tool.md +514 -0
  217. package/docs/proposals/0028-enhanced-bash-tool.md +511 -0
  218. package/docs/proposals/0029-notebook-edit-tool.md +413 -0
  219. package/docs/proposals/0030-plugin-marketplace.md +360 -0
  220. package/docs/proposals/0031-command-suggestions.md +295 -0
  221. package/docs/proposals/0032-ide-integrations.md +328 -0
  222. package/docs/proposals/0033-enterprise-deployment.md +221 -0
  223. package/docs/proposals/0034-sandboxing.md +273 -0
  224. package/docs/proposals/0035-auto-updater.md +311 -0
  225. package/docs/proposals/0036-enhanced-glob-tool.md +267 -0
  226. package/docs/proposals/0037-enhanced-grep-tool.md +360 -0
  227. package/docs/proposals/0038-interactive-cli-ui.md +373 -0
  228. package/docs/proposals/0039-streaming-enhancements.md +359 -0
  229. package/docs/proposals/0040-multi-provider-enhancements.md +369 -0
  230. package/docs/proposals/README.md +84 -0
  231. package/docs/proposals/TEMPLATE.md +57 -0
  232. package/docs/proposals/research/claude-code-research.md +307 -0
  233. package/examples/agent-demo.ts +115 -0
  234. package/examples/basic.ts +166 -0
  235. package/package.json +50 -0
  236. package/src/agent/agent.ts +276 -0
  237. package/src/agent/index.ts +6 -0
  238. package/src/agent/types.ts +62 -0
  239. package/src/cli/components/App.tsx +565 -0
  240. package/src/cli/components/CommandSuggestions.tsx +58 -0
  241. package/src/cli/components/Header.tsx +36 -0
  242. package/src/cli/components/Input.tsx +60 -0
  243. package/src/cli/components/Logo.tsx +16 -0
  244. package/src/cli/components/Messages.tsx +210 -0
  245. package/src/cli/components/ModelSelector.tsx +135 -0
  246. package/src/cli/components/Spinner.tsx +72 -0
  247. package/src/cli/components/index.ts +21 -0
  248. package/src/cli/components/theme.ts +36 -0
  249. package/src/cli/index.tsx +136 -0
  250. package/src/config/index.ts +7 -0
  251. package/src/config/manager.ts +77 -0
  252. package/src/config/types.ts +25 -0
  253. package/src/index.ts +86 -0
  254. package/src/permissions/index.ts +7 -0
  255. package/src/permissions/manager.ts +97 -0
  256. package/src/permissions/types.ts +29 -0
  257. package/src/providers/anthropic.ts +224 -0
  258. package/src/providers/gemini.ts +295 -0
  259. package/src/providers/index.ts +97 -0
  260. package/src/providers/openai.ts +261 -0
  261. package/src/providers/types.ts +181 -0
  262. package/src/session/index.ts +6 -0
  263. package/src/session/manager.ts +354 -0
  264. package/src/session/types.ts +49 -0
  265. package/src/tools/builtin/bash.ts +92 -0
  266. package/src/tools/builtin/edit.ts +37 -0
  267. package/src/tools/builtin/glob.ts +42 -0
  268. package/src/tools/builtin/grep.ts +67 -0
  269. package/src/tools/builtin/read.ts +34 -0
  270. package/src/tools/builtin/write.ts +27 -0
  271. package/src/tools/index.ts +36 -0
  272. package/src/tools/registry.ts +83 -0
  273. package/src/tools/types.ts +172 -0
  274. package/tsconfig.json +21 -0
@@ -0,0 +1,565 @@
1
+ /**
2
+ * Main App Component - Compact Ink-based TUI
3
+ * Inspired by Claude Code and Gemini CLI design patterns
4
+ */
5
+ import { useState, useEffect, useCallback, useRef } from 'react';
6
+ import { Box, Text, useApp, useInput, Static } from 'ink';
7
+ import { Agent } from '../../agent/index.js';
8
+ import type { AgentConfig } from '../../agent/types.js';
9
+ import {
10
+ UserMessage,
11
+ AssistantMessage,
12
+ ToolCall,
13
+ ToolResult,
14
+ InfoMessage,
15
+ WelcomeMessage,
16
+ CompletionMessage,
17
+ } from './Messages.js';
18
+ import { Header } from './Header.js';
19
+ import { ProgressBar } from './Spinner.js';
20
+ import { PromptInput, ConfirmPrompt } from './Input.js';
21
+ import { ModelSelector } from './ModelSelector.js';
22
+ import { CommandSuggestions, getFilteredCommands } from './CommandSuggestions.js';
23
+ import { colors, icons } from './theme.js';
24
+
25
+ // Types
26
+ interface HistoryItem {
27
+ id: string;
28
+ type: 'header' | 'welcome' | 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'info' | 'completion';
29
+ content: string;
30
+ meta?: Record<string, unknown>;
31
+ }
32
+
33
+ interface ConfirmState {
34
+ tool: string;
35
+ input: Record<string, unknown>;
36
+ resolve: (confirmed: boolean) => void;
37
+ }
38
+
39
+ interface SettingsManager {
40
+ save: (settings: { model?: string }) => Promise<void>;
41
+ }
42
+
43
+ interface Session {
44
+ id: string;
45
+ title: string;
46
+ updatedAt: string;
47
+ }
48
+
49
+ interface AppProps {
50
+ config: AgentConfig;
51
+ settingsManager?: SettingsManager;
52
+ resumeLatest?: boolean;
53
+ }
54
+
55
+ // ============================================================================
56
+ // Hooks
57
+ // ============================================================================
58
+ function useAgent(config: AgentConfig) {
59
+ const [agent] = useState(() => new Agent(config));
60
+ return agent;
61
+ }
62
+
63
+ // ============================================================================
64
+ // Help Component
65
+ // ============================================================================
66
+ function HelpPanel() {
67
+ const commands: [string, string][] = [
68
+ ['/model [name]', 'Switch model'],
69
+ ['/sessions', 'List sessions'],
70
+ ['/resume [n]', 'Resume session'],
71
+ ['/new', 'New session'],
72
+ ['/save', 'Save session'],
73
+ ['/clear', 'Clear chat'],
74
+ ['/init', 'Generate AGENT.md'],
75
+ ['/memory', 'Show memory files'],
76
+ ];
77
+
78
+ return (
79
+ <Box flexDirection="column">
80
+ {commands.map(([cmd, desc]) => (
81
+ <Text key={cmd}>
82
+ <Text color={colors.primary}>{cmd.padEnd(14)}</Text>
83
+ <Text color={colors.textMuted}>{desc}</Text>
84
+ </Text>
85
+ ))}
86
+ </Box>
87
+ );
88
+ }
89
+
90
+ interface SessionsTableProps {
91
+ sessions: Session[];
92
+ }
93
+
94
+ function SessionsTable({ sessions }: SessionsTableProps) {
95
+ const formatTime = (dateStr: string) => {
96
+ const diff = Date.now() - new Date(dateStr).getTime();
97
+ const mins = Math.floor(diff / 60000);
98
+ const hrs = Math.floor(mins / 60);
99
+ const days = Math.floor(hrs / 24);
100
+ if (mins < 60) return `${mins}m`;
101
+ if (hrs < 24) return `${hrs}h`;
102
+ return `${days}d`;
103
+ };
104
+
105
+ return (
106
+ <Box flexDirection="column">
107
+ {sessions.slice(0, 6).map((s, i) => (
108
+ <Text key={s.id}>
109
+ <Text color={colors.textMuted}>{String(i + 1).padEnd(2)}</Text>
110
+ <Text color={colors.primary}>{s.id.slice(0, 7).padEnd(8)}</Text>
111
+ <Text>{s.title.slice(0, 25).padEnd(26)}</Text>
112
+ <Text color={colors.textMuted}>{formatTime(s.updatedAt)}</Text>
113
+ </Text>
114
+ ))}
115
+ </Box>
116
+ );
117
+ }
118
+
119
+ // ============================================================================
120
+ // Main App
121
+ // ============================================================================
122
+ export function App({ config, settingsManager, resumeLatest }: AppProps) {
123
+ const { exit } = useApp();
124
+ const agent = useAgent(config);
125
+
126
+ // Generate unique ID
127
+ const genId = () => Math.random().toString(36).slice(2);
128
+
129
+ // Initial header item
130
+ const cwd = config.cwd || process.cwd();
131
+ const home = process.env.HOME || '';
132
+ const cwdDisplay = cwd.startsWith(home) ? '~' + cwd.slice(home.length) : cwd;
133
+ const cwdShort = cwdDisplay.length > 35 ? '...' + cwdDisplay.slice(-32) : cwdDisplay;
134
+
135
+ const initialHistory: HistoryItem[] = [
136
+ {
137
+ id: 'header',
138
+ type: 'header',
139
+ content: '',
140
+ meta: { provider: config.provider, model: config.model, cwd: cwdShort },
141
+ },
142
+ {
143
+ id: 'welcome',
144
+ type: 'welcome',
145
+ content: config.model,
146
+ meta: { model: config.model },
147
+ },
148
+ ];
149
+
150
+ // State
151
+ const [history, setHistory] = useState<HistoryItem[]>(initialHistory);
152
+ const [input, setInput] = useState('');
153
+ const [isProcessing, setIsProcessing] = useState(false);
154
+ const [isThinking, setIsThinking] = useState(false);
155
+ const [streamingText, setStreamingText] = useState('');
156
+ const streamingTextRef = useRef(''); // Track current streaming text for closure
157
+ const [confirmState, setConfirmState] = useState<ConfirmState | null>(null);
158
+ const [showModelSelector, setShowModelSelector] = useState(false);
159
+ const [currentModel, setCurrentModel] = useState(config.model);
160
+ const [cmdSuggestionIndex, setCmdSuggestionIndex] = useState(0);
161
+ const [inputKey, setInputKey] = useState(0); // Force cursor to end after autocomplete
162
+
163
+ // Check if showing command suggestions
164
+ const showCmdSuggestions = input.startsWith('/') && !isProcessing;
165
+ const cmdSuggestions = showCmdSuggestions ? getFilteredCommands(input) : [];
166
+
167
+ // Reset suggestion index when input changes
168
+ useEffect(() => {
169
+ setCmdSuggestionIndex(0);
170
+ }, [input]);
171
+
172
+ // Add to history
173
+ const addHistory = useCallback((item: Omit<HistoryItem, 'id'>) => {
174
+ setHistory((prev) => [...prev, { ...item, id: genId() }]);
175
+ }, []);
176
+
177
+ // Initialize
178
+ useEffect(() => {
179
+ const init = async () => {
180
+ agent.setConfirmCallback(async (tool: string, toolInput: unknown) => {
181
+ return new Promise<boolean>((resolve) => {
182
+ setConfirmState({ tool, input: toolInput as Record<string, unknown>, resolve });
183
+ });
184
+ });
185
+
186
+ if (resumeLatest) {
187
+ const resumed = await agent.resumeLatest();
188
+ if (resumed) {
189
+ addHistory({ type: 'info', content: 'Session restored' });
190
+ }
191
+ }
192
+ };
193
+ init();
194
+ }, [agent, resumeLatest, addHistory]);
195
+
196
+ // Handle confirm
197
+ const handleConfirm = (confirmed: boolean) => {
198
+ if (confirmState) {
199
+ confirmState.resolve(confirmed);
200
+ setConfirmState(null);
201
+ }
202
+ };
203
+
204
+ // Handle model selection
205
+ const handleModelSelect = async (model: string) => {
206
+ agent.setModel(model);
207
+ setCurrentModel(model);
208
+ setShowModelSelector(false);
209
+ addHistory({ type: 'info', content: `Model: ${model}` });
210
+
211
+ // Save to settings for next startup
212
+ if (settingsManager) {
213
+ await settingsManager.save({ model });
214
+ }
215
+ };
216
+
217
+ const handleModelCancel = () => {
218
+ setShowModelSelector(false);
219
+ };
220
+
221
+ // Handle command
222
+ const handleCommand = async (cmd: string): Promise<boolean> => {
223
+ const parts = cmd.slice(1).split(/\s+/);
224
+ const command = parts[0]?.toLowerCase();
225
+ const arg = parts[1];
226
+
227
+ switch (command) {
228
+ case 'help':
229
+ addHistory({ type: 'info', content: '__HELP__' });
230
+ return true;
231
+
232
+ case 'sessions':
233
+ case 'list': {
234
+ const showAll = arg === '--all' || arg === '-a';
235
+ const sessions = await agent.getSessionManager().list({ all: showAll });
236
+ if (sessions.length === 0) {
237
+ addHistory({ type: 'info', content: 'No sessions' });
238
+ } else {
239
+ addHistory({ type: 'info', content: '__SESSIONS__', meta: { input: sessions } });
240
+ }
241
+ return true;
242
+ }
243
+
244
+ case 'resume': {
245
+ let success = false;
246
+ if (arg) {
247
+ const index = parseInt(arg, 10);
248
+ if (!isNaN(index)) {
249
+ const sessions = await agent.listSessions();
250
+ if (index >= 1 && index <= sessions.length) {
251
+ success = await agent.resumeSession(sessions[index - 1].id);
252
+ }
253
+ } else {
254
+ success = await agent.resumeSession(arg);
255
+ }
256
+ } else {
257
+ success = await agent.resumeLatest();
258
+ }
259
+ addHistory({ type: 'info', content: success ? 'Restored' : 'Failed' });
260
+ return true;
261
+ }
262
+
263
+ case 'new': {
264
+ const title = parts.slice(1).join(' ') || undefined;
265
+ const sessionId = await agent.startSession(title);
266
+ addHistory({ type: 'info', content: `New: ${sessionId.slice(0, 8)}` });
267
+ return true;
268
+ }
269
+
270
+ case 'save':
271
+ await agent.saveSession();
272
+ addHistory({ type: 'info', content: 'Saved' });
273
+ return true;
274
+
275
+ case 'info': {
276
+ const sessionId = agent.getSessionId();
277
+ addHistory({
278
+ type: 'info',
279
+ content: sessionId
280
+ ? `${sessionId.slice(0, 8)} · ${agent.getHistory().length} msgs`
281
+ : 'No session',
282
+ });
283
+ return true;
284
+ }
285
+
286
+ case 'clear':
287
+ agent.clearHistory();
288
+ await agent.startSession();
289
+ setHistory(initialHistory);
290
+ return true;
291
+
292
+ case 'model': {
293
+ if (arg) {
294
+ // Direct model switch: /model gpt-4o
295
+ agent.setModel(arg);
296
+ setCurrentModel(arg);
297
+ addHistory({ type: 'info', content: `Model: ${arg}` });
298
+ } else {
299
+ // Show interactive model selector
300
+ setShowModelSelector(true);
301
+ }
302
+ return true;
303
+ }
304
+
305
+ case 'init': {
306
+ addHistory({ type: 'info', content: '/init command not available in this version' });
307
+ return true;
308
+ }
309
+
310
+ case 'memory': {
311
+ addHistory({ type: 'info', content: '/memory command not available in this version' });
312
+ return true;
313
+ }
314
+
315
+ default:
316
+ return false;
317
+ }
318
+ };
319
+
320
+ // Interrupt ref for ESC handling
321
+ const interruptFlagRef = useRef(false);
322
+
323
+ // Run agent
324
+ const runAgent = async (prompt: string) => {
325
+ setIsProcessing(true);
326
+ setIsThinking(true);
327
+ setStreamingText('');
328
+ streamingTextRef.current = '';
329
+ interruptFlagRef.current = false;
330
+ const startTime = Date.now();
331
+
332
+ try {
333
+ for await (const event of agent.run(prompt)) {
334
+ // Check for interrupt
335
+ if (interruptFlagRef.current) {
336
+ break;
337
+ }
338
+
339
+ switch (event.type) {
340
+ case 'text':
341
+ setIsThinking(false);
342
+ streamingTextRef.current += event.text;
343
+ setStreamingText(streamingTextRef.current);
344
+ break;
345
+
346
+ case 'tool_start':
347
+ setIsThinking(false);
348
+ if (streamingTextRef.current) {
349
+ addHistory({ type: 'assistant', content: streamingTextRef.current });
350
+ streamingTextRef.current = '';
351
+ setStreamingText('');
352
+ }
353
+ addHistory({
354
+ type: 'tool_call',
355
+ content: event.name,
356
+ meta: { toolName: event.name, input: event.input },
357
+ });
358
+ break;
359
+
360
+ case 'tool_result':
361
+ addHistory({
362
+ type: 'tool_result',
363
+ content: event.result.output,
364
+ meta: { toolName: event.name, success: event.result.success },
365
+ });
366
+ setIsThinking(true);
367
+ break;
368
+
369
+ case 'error':
370
+ setIsThinking(false);
371
+ addHistory({ type: 'info', content: `Error: ${event.error.message}` });
372
+ break;
373
+
374
+ case 'done':
375
+ if (streamingTextRef.current) {
376
+ addHistory({ type: 'assistant', content: streamingTextRef.current });
377
+ streamingTextRef.current = '';
378
+ setStreamingText('');
379
+ }
380
+ // Add completion message with duration
381
+ const durationMs = Date.now() - startTime;
382
+ addHistory({ type: 'completion', content: '', meta: { durationMs } });
383
+ break;
384
+ }
385
+ }
386
+ } catch (error) {
387
+ addHistory({
388
+ type: 'info',
389
+ content: `Error: ${error instanceof Error ? error.message : String(error)}`,
390
+ });
391
+ }
392
+
393
+ setIsProcessing(false);
394
+ setIsThinking(false);
395
+ };
396
+
397
+ // Handle submit
398
+ const handleSubmit = async (text: string) => {
399
+ const trimmed = text.trim();
400
+ if (!trimmed) return;
401
+
402
+ // Auto-complete command on Enter if no exact match
403
+ if (trimmed.startsWith('/') && cmdSuggestions.length > 0) {
404
+ const exactMatch = cmdSuggestions.find(
405
+ (c) => c.name === trimmed || c.name.startsWith(trimmed + ' ')
406
+ );
407
+ if (!exactMatch) {
408
+ // No exact match, complete to best match
409
+ const bestMatch = cmdSuggestions[cmdSuggestionIndex];
410
+ setInput(bestMatch.name + ' ');
411
+ setInputKey((k) => k + 1); // Force cursor to end
412
+ return;
413
+ }
414
+ }
415
+
416
+ setInput('');
417
+
418
+ if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
419
+ await agent.saveSession();
420
+ setTimeout(() => exit(), 50);
421
+ return;
422
+ }
423
+
424
+ if (trimmed.startsWith('/')) {
425
+ const handled = await handleCommand(trimmed);
426
+ if (!handled) {
427
+ addHistory({ type: 'info', content: `Unknown: ${trimmed}` });
428
+ }
429
+ return;
430
+ }
431
+
432
+ addHistory({ type: 'user', content: trimmed });
433
+ await runAgent(trimmed);
434
+ };
435
+
436
+ // Keyboard shortcuts
437
+ useInput((inputChar, key) => {
438
+ if (key.ctrl && inputChar === 'c') {
439
+ agent.saveSession().then(() => exit());
440
+ }
441
+
442
+ // ESC to interrupt processing
443
+ if (key.escape && isProcessing) {
444
+ interruptFlagRef.current = true;
445
+ setIsProcessing(false);
446
+ setStreamingText('');
447
+ streamingTextRef.current = '';
448
+ addHistory({ type: 'info', content: 'Interrupted' });
449
+ }
450
+
451
+ // Command suggestion navigation
452
+ if (showCmdSuggestions && cmdSuggestions.length > 0) {
453
+ if (key.upArrow) {
454
+ setCmdSuggestionIndex((i) => Math.max(0, i - 1));
455
+ } else if (key.downArrow) {
456
+ setCmdSuggestionIndex((i) => Math.min(cmdSuggestions.length - 1, i + 1));
457
+ } else if (key.tab) {
458
+ // Autocomplete with selected suggestion
459
+ const selected = cmdSuggestions[cmdSuggestionIndex];
460
+ if (selected) {
461
+ setInput(selected.name + ' ');
462
+ setCmdSuggestionIndex(0);
463
+ setInputKey((k) => k + 1); // Force cursor to end
464
+ }
465
+ }
466
+ }
467
+ });
468
+
469
+ // Render history item
470
+ const renderHistoryItem = (item: HistoryItem) => {
471
+ switch (item.type) {
472
+ case 'header':
473
+ return (
474
+ <Header
475
+ provider={(item.meta?.provider as string) || ''}
476
+ model={(item.meta?.model as string) || ''}
477
+ cwd={(item.meta?.cwd as string) || ''}
478
+ />
479
+ );
480
+ case 'welcome':
481
+ return <WelcomeMessage model={(item.meta?.model as string) || item.content} />;
482
+ case 'user':
483
+ return <UserMessage text={item.content} />;
484
+ case 'assistant':
485
+ return <AssistantMessage text={item.content} />;
486
+ case 'tool_call':
487
+ return (
488
+ <ToolCall
489
+ name={(item.meta?.toolName as string) || ''}
490
+ input={item.meta?.input as Record<string, unknown>}
491
+ />
492
+ );
493
+ case 'tool_result':
494
+ return (
495
+ <ToolResult
496
+ name={(item.meta?.toolName as string) || ''}
497
+ success={(item.meta?.success as boolean) ?? true}
498
+ output={item.content}
499
+ />
500
+ );
501
+ case 'info':
502
+ if (item.content === '__HELP__') return <HelpPanel />;
503
+ if (item.content === '__SESSIONS__' && item.meta?.input) {
504
+ return <SessionsTable sessions={item.meta.input as Session[]} />;
505
+ }
506
+ return <InfoMessage text={item.content} />;
507
+ case 'completion':
508
+ return <CompletionMessage durationMs={(item.meta?.durationMs as number) || 0} />;
509
+ default:
510
+ return null;
511
+ }
512
+ };
513
+
514
+ return (
515
+ <Box flexDirection="column">
516
+ <Static items={history}>
517
+ {(item) => <Box key={item.id}>{renderHistoryItem(item)}</Box>}
518
+ </Static>
519
+
520
+ {streamingText && <AssistantMessage text={streamingText} streaming />}
521
+
522
+ {confirmState && (
523
+ <Box flexDirection="column" marginTop={1}>
524
+ <Text color={colors.warning}>
525
+ {icons.warning} {confirmState.tool}
526
+ </Text>
527
+ <ConfirmPrompt message="Allow?" onConfirm={handleConfirm} />
528
+ </Box>
529
+ )}
530
+
531
+ {showModelSelector && (
532
+ <Box marginTop={1}>
533
+ <ModelSelector
534
+ currentModel={currentModel}
535
+ onSelect={handleModelSelect}
536
+ onCancel={handleModelCancel}
537
+ listModels={() => agent.listModels()}
538
+ />
539
+ </Box>
540
+ )}
541
+
542
+ {!confirmState && !showModelSelector && (
543
+ <Box flexDirection="column" marginTop={1}>
544
+ <PromptInput
545
+ key={inputKey}
546
+ value={input}
547
+ onChange={setInput}
548
+ onSubmit={handleSubmit}
549
+ />
550
+ {showCmdSuggestions && cmdSuggestions.length > 0 && (
551
+ <CommandSuggestions input={input} selectedIndex={cmdSuggestionIndex} />
552
+ )}
553
+ </Box>
554
+ )}
555
+
556
+ {isProcessing ? (
557
+ <ProgressBar />
558
+ ) : showCmdSuggestions && cmdSuggestions.length > 0 ? (
559
+ <Box marginTop={1}>
560
+ <Text color={colors.textMuted}> Tab to complete · ↑↓ navigate</Text>
561
+ </Box>
562
+ ) : null}
563
+ </Box>
564
+ );
565
+ }
@@ -0,0 +1,58 @@
1
+ import { Box, Text } from 'ink';
2
+ import { colors } from './theme.js';
3
+
4
+ interface Command {
5
+ name: string;
6
+ description: string;
7
+ }
8
+
9
+ export const COMMANDS: Command[] = [
10
+ { name: '/model', description: 'Switch model' },
11
+ { name: '/sessions', description: 'List sessions' },
12
+ { name: '/resume', description: 'Resume session' },
13
+ { name: '/new', description: 'New session' },
14
+ { name: '/save', description: 'Save session' },
15
+ { name: '/clear', description: 'Clear chat' },
16
+ { name: '/info', description: 'Session info' },
17
+ { name: '/help', description: 'Show help' },
18
+ { name: '/init', description: 'Generate AGENT.md' },
19
+ { name: '/memory', description: 'Show memory files' },
20
+ ];
21
+
22
+ interface CommandSuggestionsProps {
23
+ input: string;
24
+ selectedIndex: number;
25
+ }
26
+
27
+ export function CommandSuggestions({ input, selectedIndex }: CommandSuggestionsProps) {
28
+ // Filter commands matching input
29
+ const prefix = input.toLowerCase();
30
+ const suggestions = COMMANDS.filter((cmd) =>
31
+ cmd.name.toLowerCase().startsWith(prefix)
32
+ );
33
+
34
+ if (suggestions.length === 0) {
35
+ return null;
36
+ }
37
+
38
+ return (
39
+ <Box flexDirection="column" marginLeft={2}>
40
+ {suggestions.map((cmd, i) => {
41
+ const isSelected = i === selectedIndex;
42
+ return (
43
+ <Box key={cmd.name}>
44
+ <Text color={isSelected ? colors.primary : colors.textSecondary}>
45
+ {cmd.name.padEnd(12)}
46
+ </Text>
47
+ <Text color={colors.textMuted}>{cmd.description}</Text>
48
+ </Box>
49
+ );
50
+ })}
51
+ </Box>
52
+ );
53
+ }
54
+
55
+ export function getFilteredCommands(input: string): Command[] {
56
+ const prefix = input.toLowerCase();
57
+ return COMMANDS.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix));
58
+ }
@@ -0,0 +1,36 @@
1
+ import { Box, Text } from 'ink';
2
+ import { colors } from './theme.js';
3
+ import { Logo } from './Logo.js';
4
+
5
+ interface HeaderProps {
6
+ provider: string;
7
+ model: string;
8
+ cwd: string;
9
+ }
10
+
11
+ export function Header({ provider, model, cwd }: HeaderProps) {
12
+ const home = process.env.HOME || '';
13
+ const cwdDisplay = cwd.startsWith(home) ? '~' + cwd.slice(home.length) : cwd;
14
+
15
+ return (
16
+ <Box flexDirection="row" marginBottom={1} marginTop={1}>
17
+ <Logo />
18
+ <Box flexDirection="column" marginLeft={1}>
19
+ <Box>
20
+ <Text bold color={colors.text}>gencode </Text>
21
+ <Text color={colors.textMuted}>v0.1.0</Text>
22
+ </Box>
23
+ <Text color={colors.textMuted}>{model} · API Usage Billing</Text>
24
+ <Text color={colors.textMuted}>{cwdDisplay}</Text>
25
+ </Box>
26
+ </Box>
27
+ );
28
+ }
29
+
30
+ export function Welcome() {
31
+ return (
32
+ <Text color={colors.textMuted}>
33
+ Type a message or /help. Ctrl+C to exit.
34
+ </Text>
35
+ );
36
+ }