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.
- package/.env.example +11 -0
- package/CLAUDE.md +70 -0
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/agent/agent.d.ts +84 -0
- package/dist/agent/agent.d.ts.map +1 -0
- package/dist/agent/agent.js +233 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/index.d.ts +6 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/types.d.ts +47 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +5 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/components/App.d.ts +14 -0
- package/dist/cli/components/App.d.ts.map +1 -0
- package/dist/cli/components/App.js +395 -0
- package/dist/cli/components/App.js.map +1 -0
- package/dist/cli/components/CommandSuggestions.d.ts +13 -0
- package/dist/cli/components/CommandSuggestions.d.ts.map +1 -0
- package/dist/cli/components/CommandSuggestions.js +32 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -0
- package/dist/cli/components/Header.d.ts +9 -0
- package/dist/cli/components/Header.d.ts.map +1 -0
- package/dist/cli/components/Header.js +13 -0
- package/dist/cli/components/Header.js.map +1 -0
- package/dist/cli/components/Input.d.ts +13 -0
- package/dist/cli/components/Input.d.ts.map +1 -0
- package/dist/cli/components/Input.js +27 -0
- package/dist/cli/components/Input.js.map +1 -0
- package/dist/cli/components/Logo.d.ts +2 -0
- package/dist/cli/components/Logo.d.ts.map +1 -0
- package/dist/cli/components/Logo.js +8 -0
- package/dist/cli/components/Logo.js.map +1 -0
- package/dist/cli/components/Messages.d.ts +37 -0
- package/dist/cli/components/Messages.d.ts.map +1 -0
- package/dist/cli/components/Messages.js +106 -0
- package/dist/cli/components/Messages.js.map +1 -0
- package/dist/cli/components/ModelSelector.d.ts +13 -0
- package/dist/cli/components/ModelSelector.d.ts.map +1 -0
- package/dist/cli/components/ModelSelector.js +72 -0
- package/dist/cli/components/ModelSelector.js.map +1 -0
- package/dist/cli/components/Spinner.d.ts +12 -0
- package/dist/cli/components/Spinner.d.ts.map +1 -0
- package/dist/cli/components/Spinner.js +45 -0
- package/dist/cli/components/Spinner.js.map +1 -0
- package/dist/cli/components/index.d.ts +12 -0
- package/dist/cli/components/index.d.ts.map +1 -0
- package/dist/cli/components/index.js +12 -0
- package/dist/cli/components/index.js.map +1 -0
- package/dist/cli/components/theme.d.ts +31 -0
- package/dist/cli/components/theme.d.ts.map +1 -0
- package/dist/cli/components/theme.js +36 -0
- package/dist/cli/components/theme.js.map +1 -0
- package/dist/cli/index-legacy.d.ts +7 -0
- package/dist/cli/index-legacy.d.ts.map +1 -0
- package/dist/cli/index-legacy.js +431 -0
- package/dist/cli/index-legacy.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +116 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/ink-cli.d.ts +7 -0
- package/dist/cli/ink-cli.d.ts.map +1 -0
- package/dist/cli/ink-cli.js +105 -0
- package/dist/cli/ink-cli.js.map +1 -0
- package/dist/cli/session-picker.d.ts +16 -0
- package/dist/cli/session-picker.d.ts.map +1 -0
- package/dist/cli/session-picker.js +280 -0
- package/dist/cli/session-picker.js.map +1 -0
- package/dist/cli/ui.d.ts +61 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +364 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/manager.d.ts +31 -0
- package/dist/config/manager.d.ts.map +1 -0
- package/dist/config/manager.js +65 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/types.d.ts +22 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +6 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/index.d.ts +10 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +9 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/init.d.ts +20 -0
- package/dist/memory/init.d.ts.map +1 -0
- package/dist/memory/init.js +332 -0
- package/dist/memory/init.js.map +1 -0
- package/dist/memory/manager.d.ts +85 -0
- package/dist/memory/manager.d.ts.map +1 -0
- package/dist/memory/manager.js +234 -0
- package/dist/memory/manager.js.map +1 -0
- package/dist/memory/types.d.ts +74 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +6 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/permissions/index.d.ts +7 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +6 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/permissions/manager.d.ts +32 -0
- package/dist/permissions/manager.d.ts.map +1 -0
- package/dist/permissions/manager.js +79 -0
- package/dist/permissions/manager.js.map +1 -0
- package/dist/permissions/types.d.ts +14 -0
- package/dist/permissions/types.d.ts.map +1 -0
- package/dist/permissions/types.js +17 -0
- package/dist/permissions/types.js.map +1 -0
- package/dist/providers/anthropic.d.ts +20 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +185 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/gemini.d.ts +21 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +241 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/index.d.ts +34 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +72 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai.d.ts +19 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +221 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/types.d.ts +125 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +6 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/session/index.d.ts +6 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +6 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/manager.d.ts +101 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +295 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/session/types.d.ts +39 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +10 -0
- package/dist/session/types.js.map +1 -0
- package/dist/tools/builtin/bash.d.ts +7 -0
- package/dist/tools/builtin/bash.d.ts.map +1 -0
- package/dist/tools/builtin/bash.js +80 -0
- package/dist/tools/builtin/bash.js.map +1 -0
- package/dist/tools/builtin/edit.d.ts +7 -0
- package/dist/tools/builtin/edit.d.ts.map +1 -0
- package/dist/tools/builtin/edit.js +32 -0
- package/dist/tools/builtin/edit.js.map +1 -0
- package/dist/tools/builtin/glob.d.ts +7 -0
- package/dist/tools/builtin/glob.d.ts.map +1 -0
- package/dist/tools/builtin/glob.js +36 -0
- package/dist/tools/builtin/glob.js.map +1 -0
- package/dist/tools/builtin/grep.d.ts +7 -0
- package/dist/tools/builtin/grep.d.ts.map +1 -0
- package/dist/tools/builtin/grep.js +59 -0
- package/dist/tools/builtin/grep.js.map +1 -0
- package/dist/tools/builtin/read.d.ts +7 -0
- package/dist/tools/builtin/read.d.ts.map +1 -0
- package/dist/tools/builtin/read.js +29 -0
- package/dist/tools/builtin/read.js.map +1 -0
- package/dist/tools/builtin/write.d.ts +7 -0
- package/dist/tools/builtin/write.d.ts.map +1 -0
- package/dist/tools/builtin/write.js +24 -0
- package/dist/tools/builtin/write.js.map +1 -0
- package/dist/tools/index.d.ts +38 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +32 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +22 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +71 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +62 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +126 -0
- package/dist/tools/types.js.map +1 -0
- package/docs/README.md +16 -0
- package/docs/proposals/0001-web-fetch-tool.md +293 -0
- package/docs/proposals/0002-web-search-tool.md +306 -0
- package/docs/proposals/0003-task-subagents.md +333 -0
- package/docs/proposals/0004-plan-mode.md +338 -0
- package/docs/proposals/0005-todo-system.md +299 -0
- package/docs/proposals/0006-memory-system.md +539 -0
- package/docs/proposals/0007-context-management.md +429 -0
- package/docs/proposals/0008-checkpointing.md +327 -0
- package/docs/proposals/0009-hooks-system.md +343 -0
- package/docs/proposals/0010-mcp-integration.md +382 -0
- package/docs/proposals/0011-custom-commands.md +374 -0
- package/docs/proposals/0012-ask-user-question.md +317 -0
- package/docs/proposals/0013-multi-edit-tool.md +345 -0
- package/docs/proposals/0014-lsp-tool.md +478 -0
- package/docs/proposals/0015-ls-tool.md +407 -0
- package/docs/proposals/0016-kill-shell-tool.md +455 -0
- package/docs/proposals/0017-background-tasks.md +489 -0
- package/docs/proposals/0018-parallel-tool-execution.md +415 -0
- package/docs/proposals/0019-session-enhancements.md +462 -0
- package/docs/proposals/0020-session-summarization.md +447 -0
- package/docs/proposals/0021-skills-system.md +409 -0
- package/docs/proposals/0022-plugin-system.md +467 -0
- package/docs/proposals/0023-permission-enhancements.md +470 -0
- package/docs/proposals/0024-keyboard-shortcuts.md +443 -0
- package/docs/proposals/0025-cost-tracking.md +447 -0
- package/docs/proposals/0026-git-integration.md +475 -0
- package/docs/proposals/0027-enhanced-read-tool.md +514 -0
- package/docs/proposals/0028-enhanced-bash-tool.md +511 -0
- package/docs/proposals/0029-notebook-edit-tool.md +413 -0
- package/docs/proposals/0030-plugin-marketplace.md +360 -0
- package/docs/proposals/0031-command-suggestions.md +295 -0
- package/docs/proposals/0032-ide-integrations.md +328 -0
- package/docs/proposals/0033-enterprise-deployment.md +221 -0
- package/docs/proposals/0034-sandboxing.md +273 -0
- package/docs/proposals/0035-auto-updater.md +311 -0
- package/docs/proposals/0036-enhanced-glob-tool.md +267 -0
- package/docs/proposals/0037-enhanced-grep-tool.md +360 -0
- package/docs/proposals/0038-interactive-cli-ui.md +373 -0
- package/docs/proposals/0039-streaming-enhancements.md +359 -0
- package/docs/proposals/0040-multi-provider-enhancements.md +369 -0
- package/docs/proposals/README.md +84 -0
- package/docs/proposals/TEMPLATE.md +57 -0
- package/docs/proposals/research/claude-code-research.md +307 -0
- package/examples/agent-demo.ts +115 -0
- package/examples/basic.ts +166 -0
- package/package.json +50 -0
- package/src/agent/agent.ts +276 -0
- package/src/agent/index.ts +6 -0
- package/src/agent/types.ts +62 -0
- package/src/cli/components/App.tsx +565 -0
- package/src/cli/components/CommandSuggestions.tsx +58 -0
- package/src/cli/components/Header.tsx +36 -0
- package/src/cli/components/Input.tsx +60 -0
- package/src/cli/components/Logo.tsx +16 -0
- package/src/cli/components/Messages.tsx +210 -0
- package/src/cli/components/ModelSelector.tsx +135 -0
- package/src/cli/components/Spinner.tsx +72 -0
- package/src/cli/components/index.ts +21 -0
- package/src/cli/components/theme.ts +36 -0
- package/src/cli/index.tsx +136 -0
- package/src/config/index.ts +7 -0
- package/src/config/manager.ts +77 -0
- package/src/config/types.ts +25 -0
- package/src/index.ts +86 -0
- package/src/permissions/index.ts +7 -0
- package/src/permissions/manager.ts +97 -0
- package/src/permissions/types.ts +29 -0
- package/src/providers/anthropic.ts +224 -0
- package/src/providers/gemini.ts +295 -0
- package/src/providers/index.ts +97 -0
- package/src/providers/openai.ts +261 -0
- package/src/providers/types.ts +181 -0
- package/src/session/index.ts +6 -0
- package/src/session/manager.ts +354 -0
- package/src/session/types.ts +49 -0
- package/src/tools/builtin/bash.ts +92 -0
- package/src/tools/builtin/edit.ts +37 -0
- package/src/tools/builtin/glob.ts +42 -0
- package/src/tools/builtin/grep.ts +67 -0
- package/src/tools/builtin/read.ts +34 -0
- package/src/tools/builtin/write.ts +27 -0
- package/src/tools/index.ts +36 -0
- package/src/tools/registry.ts +83 -0
- package/src/tools/types.ts +172 -0
- 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
|
+
}
|