@benzsiangco/jarvis 1.0.2 → 1.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/dist/cli.js +478 -347
- package/dist/electron/main.js +160 -0
- package/dist/electron/preload.js +19 -0
- package/package.json +19 -6
- package/skills.md +147 -0
- package/src/agents/index.ts +248 -0
- package/src/brain/loader.ts +136 -0
- package/src/cli.ts +411 -0
- package/src/config/index.ts +363 -0
- package/src/core/executor.ts +222 -0
- package/src/core/plugins.ts +148 -0
- package/src/core/types.ts +217 -0
- package/src/electron/main.ts +192 -0
- package/src/electron/preload.ts +25 -0
- package/src/electron/types.d.ts +20 -0
- package/src/index.ts +12 -0
- package/src/providers/antigravity-loader.ts +233 -0
- package/src/providers/antigravity.ts +585 -0
- package/src/providers/index.ts +523 -0
- package/src/sessions/index.ts +194 -0
- package/src/tools/index.ts +436 -0
- package/src/tui/index.tsx +784 -0
- package/src/utils/auth-prompt.ts +394 -0
- package/src/utils/index.ts +180 -0
- package/src/utils/native-picker.ts +71 -0
- package/src/utils/skills.ts +99 -0
- package/src/utils/table-integration-examples.ts +617 -0
- package/src/utils/table-utils.ts +401 -0
- package/src/web/build-ui.ts +27 -0
- package/src/web/server.ts +674 -0
- package/src/web/ui/dist/.gitkeep +0 -0
- package/src/web/ui/dist/main.css +1 -0
- package/src/web/ui/dist/main.js +320 -0
- package/src/web/ui/dist/main.js.map +20 -0
- package/src/web/ui/index.html +46 -0
- package/src/web/ui/src/App.tsx +143 -0
- package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
- package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
- package/src/web/ui/src/components/Layout/Header.tsx +91 -0
- package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
- package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
- package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
- package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
- package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
- package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
- package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
- package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
- package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
- package/src/web/ui/src/config/models.ts +70 -0
- package/src/web/ui/src/main.tsx +13 -0
- package/src/web/ui/src/store/agentStore.ts +41 -0
- package/src/web/ui/src/store/uiStore.ts +64 -0
- package/src/web/ui/src/types/index.ts +54 -0
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
3
|
+
import { createRoot, useKeyboard, useTerminalDimensions } from '@opentui/react';
|
|
4
|
+
import { createCliRenderer, TextAttributes } from '@opentui/core';
|
|
5
|
+
import { marked } from 'marked';
|
|
6
|
+
import TerminalRenderer from 'marked-terminal';
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import type { Session, AgentConfig } from '../core/types.js';
|
|
9
|
+
import { createSession, getSession } from '../sessions/index.js';
|
|
10
|
+
import { getAgent, getPrimaryAgents, initializeAgents } from '../agents/index.js';
|
|
11
|
+
import { executeAgent } from '../core/executor.js';
|
|
12
|
+
import { loadConfig, getModelConfig, saveGlobalConfig, type JarvisConfig } from '../config/index.js';
|
|
13
|
+
import { initializeProviders, getAvailableModels, getProvidersWithStatus } from '../providers/index.js';
|
|
14
|
+
import { getActiveAccount, getAccountStats, startAntigravityLogin } from '../providers/antigravity.js';
|
|
15
|
+
import { runInteractiveAuth, providers as authProviders, saveApiKey, removeApiKey, getApiKeyStatus } from '../utils/auth-prompt.js';
|
|
16
|
+
import { loadSkills, addSkill, hasSkillsFile, type Skill } from '../utils/skills.js';
|
|
17
|
+
import { initializeTools } from '../tools/index.js';
|
|
18
|
+
|
|
19
|
+
// Configure marked to use terminal renderer
|
|
20
|
+
marked.setOptions({
|
|
21
|
+
renderer: new (TerminalRenderer as any)()
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
interface AppProps {
|
|
25
|
+
initialPrompt?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
variant?: string;
|
|
28
|
+
runInteractive?: (task: () => Promise<void>) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ToolCall {
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
command?: string;
|
|
35
|
+
output?: string;
|
|
36
|
+
expanded?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Message {
|
|
40
|
+
role: 'user' | 'assistant';
|
|
41
|
+
content: string;
|
|
42
|
+
thinking?: string;
|
|
43
|
+
agentName?: string;
|
|
44
|
+
modelName?: string;
|
|
45
|
+
duration?: number;
|
|
46
|
+
toolCalls?: ToolCall[];
|
|
47
|
+
usage?: {
|
|
48
|
+
promptTokens?: number;
|
|
49
|
+
completionTokens?: number;
|
|
50
|
+
totalTokens?: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Command {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
shortcut?: string;
|
|
59
|
+
category: string;
|
|
60
|
+
action: () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ModelWithVariants {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
provider?: string;
|
|
67
|
+
variants?: Record<string, any>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Markdown Text Component - Fixed line-by-line rendering to prevent overlap
|
|
71
|
+
function MarkdownText({ content, fg = "#ffffff" }: { content: string, fg?: string }) {
|
|
72
|
+
const { width } = useTerminalDimensions();
|
|
73
|
+
const lines = useMemo(() => {
|
|
74
|
+
try {
|
|
75
|
+
const renderer = new (TerminalRenderer as any)({
|
|
76
|
+
width: Math.max(20, width - 15),
|
|
77
|
+
reflowText: true,
|
|
78
|
+
showSectionPrefix: false,
|
|
79
|
+
});
|
|
80
|
+
const formatted = (marked.parse(content, { renderer }) as string).trim();
|
|
81
|
+
return formatted.split('\n');
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return [content];
|
|
84
|
+
}
|
|
85
|
+
}, [content, width]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<box style={{ flexDirection: 'column' }}>{lines.map((line, i) => (<text key={i} fg={fg}>{String(line)}</text>))}</box>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Thinking Block - OpenCode style
|
|
93
|
+
function ThinkingBlock({ thinking }: { thinking: string }) {
|
|
94
|
+
const lines = useMemo(() => thinking.split('\n'), [thinking]);
|
|
95
|
+
return (
|
|
96
|
+
<box style={{ marginBottom: 1, flexDirection: 'column' }}><box style={{ border: ['left'], borderStyle: 'single', borderColor: '#d97706', paddingLeft: 1, flexDirection: 'column' }}><text fg="#d97706" style={{ attributes: TextAttributes.ITALIC }}>Thinking:</text>{lines.map((line, i) => (
|
|
97
|
+
<text key={i} fg="#888888">{String(line)}</text>
|
|
98
|
+
))}</box></box>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Tool Block - Expandable terminal
|
|
103
|
+
function ToolBlock({ tool, isExpanded, isHovered, onHover, onToggle }: { tool: ToolCall; isExpanded: boolean; isHovered: boolean; onHover: () => void; onToggle: () => void; }) {
|
|
104
|
+
const outputLines = tool.output?.split('\n') || [];
|
|
105
|
+
const maxLines = 10;
|
|
106
|
+
const hasMore = outputLines.length > maxLines;
|
|
107
|
+
const displayLines = isExpanded ? outputLines : outputLines.slice(0, maxLines);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<box
|
|
111
|
+
style={{ marginBottom: 1, backgroundColor: isHovered ? '#1a1a1a' : '#0d1117', flexDirection: 'column' }}
|
|
112
|
+
onMouseMove={onHover}
|
|
113
|
+
onMouseDown={onToggle}
|
|
114
|
+
><box style={{ flexDirection: 'column', border: ['left'], borderStyle: 'single', borderColor: isHovered ? '#22d3ee' : '#06b6d4', paddingLeft: 1 }}>{tool.description ? <text fg="#666666"># {String(tool.description)}</text> : null}<box style={{ flexDirection: 'row' }}><text fg="#22c55e">$ </text><text fg="#ffffff">{String(tool.command || tool.name)}</text></box>{displayLines.length > 0 ? (
|
|
115
|
+
<box style={{ flexDirection: 'column', marginTop: 1 }}>{displayLines.map((line, i) => (
|
|
116
|
+
<text key={i} fg="#888888">{String(line)}</text>
|
|
117
|
+
))}{!isExpanded && hasMore ? <text fg="#666666">...</text> : null}</box>
|
|
118
|
+
) : null}{hasMore ? (
|
|
119
|
+
<box style={{ marginTop: 1 }}><text fg="#06b6d4">{isExpanded ? 'Click to collapse' : 'Click to expand'}</text></box>
|
|
120
|
+
) : null}</box></box>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// User Message - Cyan border text
|
|
125
|
+
function UserMessage({ content }: { content: string }) {
|
|
126
|
+
return (
|
|
127
|
+
<box style={{ marginBottom: 1 }}><box style={{ border: ['left'], borderStyle: 'single', borderColor: '#06b6d4', paddingLeft: 1 }}><MarkdownText content={content} /></box></box>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Assistant Message - Metadata and Content
|
|
132
|
+
function AssistantMessage({ message, expandedTools, onToggleTool, hoveredToolIndex, onHoverTool }: { message: Message; expandedTools: Set<number>; onToggleTool: (index: number) => void; hoveredToolIndex: number | null; onHoverTool: (index: number | null) => void; }) {
|
|
133
|
+
return (
|
|
134
|
+
<box style={{ flexDirection: 'column', marginBottom: 1 }}>
|
|
135
|
+
{message.thinking ? <ThinkingBlock thinking={message.thinking} /> : null}
|
|
136
|
+
{message.toolCalls ? message.toolCalls.map((tool, i) => (
|
|
137
|
+
<ToolBlock key={i} tool={tool} isExpanded={expandedTools.has(i)} isHovered={hoveredToolIndex === i} onHover={() => onHoverTool(i)} onToggle={() => onToggleTool(i)} />
|
|
138
|
+
)) : null}
|
|
139
|
+
<MarkdownText content={message.content} />
|
|
140
|
+
<box style={{ marginTop: 1, flexDirection: 'row' }}>
|
|
141
|
+
<text fg="#22c55e">■ </text>
|
|
142
|
+
<text fg="#22c55e">{String(message.agentName || 'Jarvis')} </text>
|
|
143
|
+
<text fg="#666666">· {String(message.modelName || 'unknown')}</text>
|
|
144
|
+
{message.duration ? <text fg="#666666"> · {String((message.duration / 1000).toFixed(1))}s</text> : null}
|
|
145
|
+
</box>
|
|
146
|
+
</box>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Modal Component - Clean OpenCode layout
|
|
151
|
+
function Modal({ title, width, height, onClose, children }: { title: string; width: number; height: number; onClose: () => void; children: React.ReactNode }) {
|
|
152
|
+
const modalWidth = Math.min(60, width - 8);
|
|
153
|
+
const modalLeft = Math.floor((width - modalWidth) / 2);
|
|
154
|
+
const modalHeight = Math.min(24, height - 4);
|
|
155
|
+
const modalTop = Math.floor((height - modalHeight) / 2);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<>
|
|
159
|
+
<box style={{ position: 'absolute', top: 0, left: 0, width, height, backgroundColor: '#000000aa' }} onMouseDown={onClose} />
|
|
160
|
+
<box style={{ position: 'absolute', top: modalTop, left: modalLeft, width: modalWidth, height: modalHeight, flexDirection: 'column', backgroundColor: '#1c1c1c', border: ['top', 'bottom', 'left', 'right'], borderStyle: 'single', borderColor: '#3a3a3a' }}><box style={{ flexDirection: 'row', paddingLeft: 1, paddingRight: 1, height: 1, borderStyle: 'single', border: ['bottom'], borderColor: '#3a3a3a' }}><box style={{ flexGrow: 1 }}><text fg="#ffffff" style={{ attributes: TextAttributes.BOLD }}>{String(title)}</text></box><box><text fg="#666666">esc</text></box></box><box style={{ flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}>{children}</box></box>
|
|
161
|
+
</>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Command Item - Same line name and shortcut
|
|
166
|
+
function CommandItem({ name, description, shortcut, isSelected, onHover, onClick }: { name: string; description?: string; shortcut?: string; isSelected: boolean; onHover: () => void; onClick: () => void }) {
|
|
167
|
+
return (
|
|
168
|
+
<box onMouseMove={onHover} onMouseDown={onClick} style={{ flexDirection: 'row', backgroundColor: isSelected ? '#f97316' : 'transparent', paddingLeft: 1, paddingRight: 1 }}><box style={{ flexGrow: 1, flexDirection: 'row' }}><text fg={isSelected ? '#000000' : '#ffffff'} style={{ attributes: isSelected ? TextAttributes.BOLD : 0 }}>{String(name)}</text>{description ? <text fg={isSelected ? '#000000' : '#888888'}> {String(description)}</text> : null}</box><box style={{ flexShrink: 0 }}>{shortcut ? <text fg={isSelected ? '#000000' : '#666666'}>{String(shortcut)}</text> : null}</box></box>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Helper to read clipboard
|
|
173
|
+
function getClipboard(): Promise<string> {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
const platform = process.platform;
|
|
176
|
+
let command = '';
|
|
177
|
+
if (platform === 'win32') command = 'powershell -command "Get-Clipboard"';
|
|
178
|
+
else if (platform === 'darwin') command = 'pbpaste';
|
|
179
|
+
else command = 'xclip -o -selection clipboard';
|
|
180
|
+
|
|
181
|
+
exec(command, (err, stdout) => {
|
|
182
|
+
if (err) resolve('');
|
|
183
|
+
else resolve(stdout.trim());
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function App({ initialPrompt, variant: initialVariant, runInteractive }: AppProps) {
|
|
189
|
+
const [config, setConfig] = useState<JarvisConfig | null>(null);
|
|
190
|
+
const [currentSession, setCurrentSession] = useState<Session | null>(null);
|
|
191
|
+
const [agent, setAgent] = useState<AgentConfig | null>(null);
|
|
192
|
+
const [inputValue, setInputValue] = useState('');
|
|
193
|
+
const [variant, setVariant] = useState<string | undefined>(initialVariant);
|
|
194
|
+
const [pastedContent, setPastedContent] = useState<string | null>(null);
|
|
195
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
196
|
+
const [history, setHistory] = useState<string[]>([]);
|
|
197
|
+
const [historyIndex, setHistoryIndex] = useState<number>(-1);
|
|
198
|
+
const [tempInput, setTempInput] = useState('');
|
|
199
|
+
const [streamingText, setStreamingText] = useState('');
|
|
200
|
+
const [streamingThinking, setStreamingThinking] = useState('');
|
|
201
|
+
const [error, setError] = useState<string | null>(null);
|
|
202
|
+
const [mode, setMode] = useState<'chat' | 'commands' | 'models' | 'agents' | 'variants' | 'providers' | 'auth' | 'settings' | 'skills'>('chat');
|
|
203
|
+
const [authStep, setAuthStep] = useState<'list' | 'method' | 'apikey'>('list');
|
|
204
|
+
const [selectedProviderId, setSelectedProviderId] = useState<string | null>(null);
|
|
205
|
+
const [selectionIndex, setSelectionIndex] = useState(0);
|
|
206
|
+
const [skills, setSkills] = useState<Skill[]>([]);
|
|
207
|
+
const [skillStep, setSkillStep] = useState<'list' | 'create'>('list');
|
|
208
|
+
const [newSkillName, setNewSkillName] = useState('');
|
|
209
|
+
const [newSkillDesc, setNewSkillDesc] = useState('');
|
|
210
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
211
|
+
const [totalTokens, setTotalTokens] = useState(0);
|
|
212
|
+
const [sessionName, setSessionName] = useState('New session');
|
|
213
|
+
const [hoveredToolIndex, setHoveredToolIndex] = useState<number | null>(null);
|
|
214
|
+
const [expandedTools, setExpandedTools] = useState<Set<number>>(new Set());
|
|
215
|
+
const [searchValue, setSearchValue] = useState('');
|
|
216
|
+
const [accountStats, setAccountStats] = useState({ active: '', count: 0, total: 0 });
|
|
217
|
+
const scrollRef = useRef<any>(null);
|
|
218
|
+
const appStartTime = useRef(Date.now());
|
|
219
|
+
const [shutdownState, setShutdownState] = useState<'none' | 'summary' | 'closing'>('none');
|
|
220
|
+
|
|
221
|
+
const { width, height } = useTerminalDimensions();
|
|
222
|
+
|
|
223
|
+
const settingsList = useMemo(() => [
|
|
224
|
+
{ id: 'bash', name: 'Execute Commands' },
|
|
225
|
+
{ id: 'read', name: 'Read Files' },
|
|
226
|
+
{ id: 'edit', name: 'Edit Files' },
|
|
227
|
+
{ id: 'webfetch', name: 'Web Access' },
|
|
228
|
+
], []);
|
|
229
|
+
|
|
230
|
+
const togglePermission = useCallback((id: string) => {
|
|
231
|
+
if (!config) return;
|
|
232
|
+
// @ts-ignore
|
|
233
|
+
const current = config.permission?.[id] || 'ask';
|
|
234
|
+
const next = current === 'ask' ? 'allow' : current === 'allow' ? 'deny' : 'ask';
|
|
235
|
+
|
|
236
|
+
const newConfig = {
|
|
237
|
+
...config,
|
|
238
|
+
permission: {
|
|
239
|
+
...config.permission,
|
|
240
|
+
[id]: next
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
setConfig(newConfig);
|
|
244
|
+
saveGlobalConfig(newConfig);
|
|
245
|
+
}, [config]);
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (shutdownState === 'summary') {
|
|
249
|
+
const timer = setTimeout(() => setShutdownState('closing'), 3500);
|
|
250
|
+
return () => clearTimeout(timer);
|
|
251
|
+
}
|
|
252
|
+
if (shutdownState === 'closing') {
|
|
253
|
+
const timer = setTimeout(() => {
|
|
254
|
+
process.stdout.write('\u001b[?25h');
|
|
255
|
+
process.stdout.write('\u001b[2J\u001b[3J\u001b[H');
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}, 1500);
|
|
258
|
+
return () => clearTimeout(timer);
|
|
259
|
+
}
|
|
260
|
+
}, [shutdownState]);
|
|
261
|
+
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
const updateStats = () => {
|
|
264
|
+
try {
|
|
265
|
+
const active = getActiveAccount();
|
|
266
|
+
const stats = getAccountStats();
|
|
267
|
+
setAccountStats({
|
|
268
|
+
active: active?.email || '',
|
|
269
|
+
count: stats.available,
|
|
270
|
+
total: stats.total
|
|
271
|
+
});
|
|
272
|
+
setSkills(loadSkills());
|
|
273
|
+
} catch (e) {
|
|
274
|
+
// Ignore errors
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
updateStats();
|
|
279
|
+
const interval = setInterval(updateStats, 2000);
|
|
280
|
+
return () => clearInterval(interval);
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
const buildCommands = useCallback((): Command[] => [
|
|
284
|
+
{ id: 'login', name: 'Jarvis Auth Login', category: 'Account', action: () => { setMode('auth'); setAuthStep('list'); setSelectionIndex(0); } },
|
|
285
|
+
{ id: 'skills', name: 'Skills', category: 'General', action: () => { setMode('skills'); setSkillStep('list'); setSelectionIndex(0); } },
|
|
286
|
+
{ id: 'settings', name: 'Settings', category: 'General', action: () => { setMode('settings'); setSelectionIndex(0); } },
|
|
287
|
+
{ id: 'show-providers', name: 'Show Providers', category: 'System', action: () => { setMode('providers'); setSelectionIndex(0); } },
|
|
288
|
+
{ id: 'switch-model', name: 'Switch model', shortcut: 'ctrl+x m', category: 'Model', action: () => { setMode('models'); setSelectionIndex(0); } },
|
|
289
|
+
{ id: 'open-editor', name: 'Open editor', shortcut: 'ctrl+x e', category: 'Session', action: () => {} },
|
|
290
|
+
{ id: 'rename-session', name: 'Rename session', shortcut: 'ctrl+r', category: 'Session', action: () => {} },
|
|
291
|
+
{ id: 'undo-previous', name: 'Undo previous message', shortcut: 'ctrl+x u', category: 'Session', action: () => {} },
|
|
292
|
+
{ id: 'clear', name: 'Clear chat', shortcut: 'ctrl+l', category: 'Quick', action: () => { setMessages([]); setTotalTokens(0); setMode('chat'); } },
|
|
293
|
+
{ id: 'new-session', name: 'New session', shortcut: 'ctrl+n', category: 'Quick', action: () => { setCurrentSession(null); setMessages([]); setTotalTokens(0); setSessionName('New session'); setMode('chat'); } },
|
|
294
|
+
{ id: 'exit', name: 'Exit', shortcut: 'ctrl+c', category: 'System', action: () => setShutdownState('summary') },
|
|
295
|
+
], []);
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const init = async () => {
|
|
299
|
+
try {
|
|
300
|
+
const cfg = loadConfig(); setConfig(cfg);
|
|
301
|
+
await initializeProviders(cfg); initializeAgents(cfg.agent); initializeTools();
|
|
302
|
+
const primaryAgents = getPrimaryAgents(); const initialAgent = primaryAgents[0] || getAgent('build');
|
|
303
|
+
if (initialAgent) setAgent(initialAgent);
|
|
304
|
+
if (initialPrompt) setInputValue(initialPrompt);
|
|
305
|
+
} catch (err) { setError(`Init Error: ${(err as Error).message}`); }
|
|
306
|
+
};
|
|
307
|
+
init();
|
|
308
|
+
}, [initialPrompt]);
|
|
309
|
+
|
|
310
|
+
const availableModels: ModelWithVariants[] = config ? getAvailableModels(config).map(m => ({ ...m, variants: getModelConfig(config, m.id)?.variants })) : [];
|
|
311
|
+
const providersList = getProvidersWithStatus();
|
|
312
|
+
const primaryAgents = getPrimaryAgents();
|
|
313
|
+
const commands = buildCommands();
|
|
314
|
+
const filteredCommands = commands;
|
|
315
|
+
|
|
316
|
+
const currentVariants = useMemo(() => {
|
|
317
|
+
if (!config?.model) return [];
|
|
318
|
+
const modelCfg = getModelConfig(config, config.model);
|
|
319
|
+
return modelCfg?.variants ? Object.keys(modelCfg.variants) : [];
|
|
320
|
+
}, [config]);
|
|
321
|
+
|
|
322
|
+
const getModelDisplayName = useCallback((modelId: string) => {
|
|
323
|
+
if (!config) return modelId;
|
|
324
|
+
return getModelConfig(config, modelId)?.name || modelId.split('/').pop() || 'unknown';
|
|
325
|
+
}, [config]);
|
|
326
|
+
|
|
327
|
+
const getProviderDisplayName = useCallback((modelId: string) => {
|
|
328
|
+
if (modelId.includes('google')) return 'Google';
|
|
329
|
+
if (modelId.includes('anthropic')) return 'Antigravity';
|
|
330
|
+
return 'OpenAI';
|
|
331
|
+
}, []);
|
|
332
|
+
|
|
333
|
+
useKeyboard((key) => {
|
|
334
|
+
if (key.ctrl && key.name === 'c') {
|
|
335
|
+
setShutdownState('summary');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (key.ctrl && key.name === 'v') { handlePaste(); return; }
|
|
339
|
+
if (mode === 'chat') {
|
|
340
|
+
if (key.ctrl && key.name === 'p') { setMode('commands'); setSelectionIndex(0); setSearchValue(''); return; }
|
|
341
|
+
if (key.ctrl && key.name === 'm') { setMode('models'); setSelectionIndex(0); return; }
|
|
342
|
+
if (key.ctrl && key.name === 't') { setMode('variants'); setSelectionIndex(0); return; }
|
|
343
|
+
if (key.name === 'tab') { const next = agent?.id === 'build' ? getAgent('plan') : getAgent('build'); if (next) setAgent(next); return; }
|
|
344
|
+
|
|
345
|
+
if (key.name === 'up') {
|
|
346
|
+
if (history.length === 0) return;
|
|
347
|
+
const newIndex = historyIndex === -1 ? history.length - 1 : Math.max(0, historyIndex - 1);
|
|
348
|
+
if (historyIndex === -1) setTempInput(inputValue);
|
|
349
|
+
setHistoryIndex(newIndex);
|
|
350
|
+
setInputValue(history[newIndex] || '');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (key.name === 'down') {
|
|
354
|
+
if (historyIndex === -1) return;
|
|
355
|
+
const newIndex = historyIndex + 1;
|
|
356
|
+
if (newIndex >= history.length) {
|
|
357
|
+
setHistoryIndex(-1);
|
|
358
|
+
setInputValue(tempInput);
|
|
359
|
+
} else {
|
|
360
|
+
setHistoryIndex(newIndex);
|
|
361
|
+
setInputValue(history[newIndex] || '');
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (mode === 'commands' || mode === 'models' || mode === 'agents' || mode === 'variants' || mode === 'providers' || mode === 'auth' || mode === 'settings' || mode === 'skills') {
|
|
367
|
+
if (key.name === 'escape') { setMode('chat'); setSearchValue(''); return; }
|
|
368
|
+
if (key.name === 'up') { setSelectionIndex((i) => Math.max(0, i - 1)); return; }
|
|
369
|
+
if (key.name === 'down') {
|
|
370
|
+
const max = mode === 'commands' ? filteredCommands.length - 1
|
|
371
|
+
: mode === 'models' ? availableModels.length - 1
|
|
372
|
+
: mode === 'agents' ? primaryAgents.length - 1
|
|
373
|
+
: mode === 'variants' ? currentVariants.length - 1
|
|
374
|
+
: mode === 'providers' ? providersList.length - 1
|
|
375
|
+
: mode === 'auth' ? (authStep === 'list' ? authProviders.length : authStep === 'method' ? 1 : 0)
|
|
376
|
+
: mode === 'settings' ? 3
|
|
377
|
+
: mode === 'skills' ? (skillStep === 'list' ? skills.length : 0)
|
|
378
|
+
: 0;
|
|
379
|
+
setSelectionIndex((i) => Math.min(max, i + 1)); return;
|
|
380
|
+
}
|
|
381
|
+
if (key.name === 'return') {
|
|
382
|
+
if (mode === 'commands') { const cmd = filteredCommands[selectionIndex]; if (cmd) cmd.action(); }
|
|
383
|
+
else if (mode === 'models') { const m = availableModels[selectionIndex]; if (m && config) { config.model = m.id; setConfig({ ...config }); setMode('chat'); } }
|
|
384
|
+
else if (mode === 'agents') { const a = primaryAgents[selectionIndex]; if (a) { setAgent(a); setMode('chat'); } }
|
|
385
|
+
else if (mode === 'variants') { const v = currentVariants[selectionIndex]; if (v) { setVariant(v); setMode('chat'); } }
|
|
386
|
+
else if (mode === 'providers') { setMode('chat'); }
|
|
387
|
+
else if (mode === 'settings') {
|
|
388
|
+
const s = settingsList[selectionIndex];
|
|
389
|
+
if (s) togglePermission(s.id);
|
|
390
|
+
}
|
|
391
|
+
else if (mode === 'skills') {
|
|
392
|
+
if (skillStep === 'list') {
|
|
393
|
+
if (selectionIndex === 0) { // Create New
|
|
394
|
+
setSkillStep('create');
|
|
395
|
+
setNewSkillName('');
|
|
396
|
+
setNewSkillDesc('');
|
|
397
|
+
setInputValue('');
|
|
398
|
+
} else {
|
|
399
|
+
const s = skills[selectionIndex - 1];
|
|
400
|
+
if (s) {
|
|
401
|
+
setInputValue((prev) => prev + (prev ? '\n' : '') + `Use skill: ${s.name}`);
|
|
402
|
+
setMode('chat');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else if (mode === 'auth') {
|
|
408
|
+
if (authStep === 'list') {
|
|
409
|
+
const p = authProviders[selectionIndex];
|
|
410
|
+
if (p) {
|
|
411
|
+
setSelectedProviderId(p.id);
|
|
412
|
+
if (p.id === 'google') {
|
|
413
|
+
setAuthStep('method');
|
|
414
|
+
setSelectionIndex(0);
|
|
415
|
+
} else {
|
|
416
|
+
setAuthStep('apikey');
|
|
417
|
+
setInputValue('');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} else if (authStep === 'method') {
|
|
421
|
+
if (selectionIndex === 0) {
|
|
422
|
+
setMode('chat');
|
|
423
|
+
startAntigravityLogin().catch(e => setError(e.message));
|
|
424
|
+
} else {
|
|
425
|
+
setAuthStep('apikey');
|
|
426
|
+
setInputValue('');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
setSearchValue('');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const handlePaste = useCallback(async () => {
|
|
436
|
+
const text = await getClipboard();
|
|
437
|
+
if (!text) return;
|
|
438
|
+
const lines = text.split('\n');
|
|
439
|
+
if (lines.length > 1 || text.length > 100) {
|
|
440
|
+
setPastedContent(text);
|
|
441
|
+
setInputValue(`Pasted ${lines.length} lines (${text.length} chars). Press Enter to send.`);
|
|
442
|
+
} else {
|
|
443
|
+
setInputValue((prev) => prev + text);
|
|
444
|
+
}
|
|
445
|
+
}, []);
|
|
446
|
+
|
|
447
|
+
const handleSubmit = useCallback(async (text: string) => {
|
|
448
|
+
if (mode === 'skills' && skillStep === 'create') {
|
|
449
|
+
if (!newSkillName) {
|
|
450
|
+
setNewSkillName(text);
|
|
451
|
+
setInputValue('');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
addSkill(newSkillName, text);
|
|
455
|
+
setSkills(loadSkills());
|
|
456
|
+
setMode('chat');
|
|
457
|
+
setInputValue('');
|
|
458
|
+
setSkillStep('list');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (mode === 'auth' && authStep === 'apikey' && selectedProviderId) {
|
|
463
|
+
saveApiKey(selectedProviderId, text.trim());
|
|
464
|
+
setMode('chat');
|
|
465
|
+
setInputValue('');
|
|
466
|
+
setAuthStep('list');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let contentToSend = text;
|
|
471
|
+
if (pastedContent && text.startsWith('Pasted ')) {
|
|
472
|
+
contentToSend = pastedContent;
|
|
473
|
+
setPastedContent(null);
|
|
474
|
+
}
|
|
475
|
+
if (!agent || isLoading || !contentToSend.trim()) return;
|
|
476
|
+
setHistory(prev => [...prev, contentToSend]);
|
|
477
|
+
setHistoryIndex(-1);
|
|
478
|
+
setTempInput('');
|
|
479
|
+
let sess = currentSession; if (!sess) { sess = createSession(agent.id); setCurrentSession(sess); setSessionName(contentToSend.split('\n')[0]?.slice(0, 50) || 'New session'); }
|
|
480
|
+
setMessages((prev) => [...prev, { role: 'user', content: contentToSend }]); setInputValue(''); setIsLoading(true); setStreamingText(''); setStreamingThinking(''); setError(null);
|
|
481
|
+
const startTime = Date.now();
|
|
482
|
+
try {
|
|
483
|
+
const result = await executeAgent(contentToSend, { session: sess, agent: { ...agent, model: config?.model }, modelOptions: { variant }, onText: (chunk) => setStreamingText((prev) => prev + chunk), onThinking: (thought) => setStreamingThinking((prev) => prev + thought) });
|
|
484
|
+
const updated = getSession(sess.id);
|
|
485
|
+
if (updated) {
|
|
486
|
+
setCurrentSession({ ...updated }); const lastMsg = updated.messages.filter((m) => m.role === 'assistant').pop();
|
|
487
|
+
if (lastMsg) {
|
|
488
|
+
setMessages((prev) => [...prev, { role: 'assistant', content: lastMsg.content as string, thinking: streamingThinking || undefined, agentName: agent.name, modelName: config?.model?.split('/').pop() || 'unknown', duration: Date.now() - startTime, usage: result.usage }]);
|
|
489
|
+
if (result.usage?.totalTokens) setTotalTokens(prev => prev + result.usage.totalTokens);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch (err) { setError((err as Error).message); } finally { setIsLoading(false); setStreamingText(''); setStreamingThinking(''); }
|
|
493
|
+
}, [agent, isLoading, currentSession, config, streamingThinking, pastedContent, variant]);
|
|
494
|
+
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
497
|
+
}, [messages.length, streamingText, streamingThinking, expandedTools]);
|
|
498
|
+
|
|
499
|
+
const costEstimate = (totalTokens / 1000000) * 0.15;
|
|
500
|
+
const usagePercent = Math.round((totalTokens / 100000) * 100) || 0;
|
|
501
|
+
const displaySessionName = sessionName.length > 25 ? sessionName.slice(0, 22) + '...' : sessionName;
|
|
502
|
+
|
|
503
|
+
const subAgentCount = useMemo(() => {
|
|
504
|
+
let count = 0;
|
|
505
|
+
for (const msg of messages) {
|
|
506
|
+
if (msg.role === 'assistant' && msg.toolCalls) {
|
|
507
|
+
for (const tc of msg.toolCalls) {
|
|
508
|
+
if (tc.name === 'task') count++;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return count;
|
|
513
|
+
}, [messages]);
|
|
514
|
+
|
|
515
|
+
if (!config || !agent) return <box style={{ width, height, alignItems: 'center', justifyContent: 'center' }}><text fg="#06b6d4">Initializing J.A.R.V.I.S. systems...</text></box>;
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<box style={{ flexDirection: 'column', width, height, backgroundColor: '#0a0a0a' }}>
|
|
519
|
+
<box style={{ paddingLeft: 1, paddingRight: 1, paddingTop: 1, height: 1, flexDirection: 'row' }}>
|
|
520
|
+
<box style={{ flexGrow: 1, overflow: 'hidden' }}>
|
|
521
|
+
<text fg="#888888"># {String(displaySessionName)}</text>
|
|
522
|
+
</box>
|
|
523
|
+
<box style={{ flexDirection: 'row', flexShrink: 0 }}>
|
|
524
|
+
<text fg="#666666">{String(totalTokens.toLocaleString())} </text>
|
|
525
|
+
<text fg="#666666">{String(usagePercent)}% </text>
|
|
526
|
+
<text fg="#666666">(${String(costEstimate.toFixed(2))}) </text>
|
|
527
|
+
<text fg="#666666">v1.0.3</text>
|
|
528
|
+
</box>
|
|
529
|
+
</box>
|
|
530
|
+
<scrollbox ref={scrollRef} style={{ flexGrow: 1, paddingLeft: 1, paddingRight: 1, paddingTop: 1 }} focused={false}>
|
|
531
|
+
<box style={{ flexDirection: 'column', minHeight: 1 }}>
|
|
532
|
+
{messages.map((msg, i) => (
|
|
533
|
+
msg.role === 'user' ? (
|
|
534
|
+
<UserMessage key={i} content={msg.content} />
|
|
535
|
+
) : (
|
|
536
|
+
<AssistantMessage key={i} message={msg} expandedTools={expandedTools} onToggleTool={(idx) => setExpandedTools(prev => {const n = new Set(prev); if(n.has(idx)) n.delete(idx); else n.add(idx); return n;})} hoveredToolIndex={hoveredToolIndex} onHoverTool={setHoveredToolIndex} />
|
|
537
|
+
)
|
|
538
|
+
))}
|
|
539
|
+
{streamingThinking ? (
|
|
540
|
+
<box style={{ marginBottom: 1 }}>
|
|
541
|
+
<box style={{ border: ['left'], borderStyle: 'single', borderColor: '#d97706', paddingLeft: 1, flexDirection: 'column' }}>
|
|
542
|
+
<text fg="#d97706" style={{ attributes: TextAttributes.ITALIC }}>Thinking:</text>
|
|
543
|
+
{String(streamingThinking).split('\n').map((line, i) => <text key={i} fg="#888888">{String(line)}</text>)}
|
|
544
|
+
</box>
|
|
545
|
+
</box>
|
|
546
|
+
) : null}
|
|
547
|
+
{streamingText ? (
|
|
548
|
+
<box style={{ marginBottom: 1 }}>
|
|
549
|
+
<MarkdownText content={streamingText} />
|
|
550
|
+
</box>
|
|
551
|
+
) : null}
|
|
552
|
+
</box>
|
|
553
|
+
</scrollbox>
|
|
554
|
+
{error ? <box style={{ padding: 1, backgroundColor: '#7f1d1d' }}><text fg="#fca5a5">{String(error)}</text></box> : null}
|
|
555
|
+
<box style={{
|
|
556
|
+
flexDirection: 'column',
|
|
557
|
+
backgroundColor: '#1a1a1a',
|
|
558
|
+
border: ['left'],
|
|
559
|
+
borderStyle: 'single',
|
|
560
|
+
borderColor: '#06b6d4',
|
|
561
|
+
marginLeft: 1,
|
|
562
|
+
marginRight: 1,
|
|
563
|
+
paddingLeft: 1,
|
|
564
|
+
paddingRight: 1,
|
|
565
|
+
paddingTop: 1,
|
|
566
|
+
paddingBottom: 1,
|
|
567
|
+
height: 5
|
|
568
|
+
}} onMouseDown={(e: any) => { if (e.button === 2) handlePaste(); }}>
|
|
569
|
+
<box style={{ height: 1 }}>
|
|
570
|
+
<input
|
|
571
|
+
focused={mode === 'chat' || (mode === 'auth' && authStep === 'apikey')}
|
|
572
|
+
value={inputValue}
|
|
573
|
+
onInput={(v) => { if (pastedContent) setPastedContent(null); setInputValue(v || ''); }}
|
|
574
|
+
onSubmit={handleSubmit}
|
|
575
|
+
placeholder={
|
|
576
|
+
mode === 'auth' && authStep === 'apikey' ? `Enter API Key for ${selectedProviderId}` :
|
|
577
|
+
isLoading ? 'Responding...' : ''
|
|
578
|
+
}
|
|
579
|
+
onMouseDown={(e: any) => { if (e.button === 2) handlePaste(); }}
|
|
580
|
+
/>
|
|
581
|
+
</box>
|
|
582
|
+
<box style={{ height: 1 }} onMouseDown={(e: any) => { if (e.button === 2) handlePaste(); }} />
|
|
583
|
+
<box style={{ flexDirection: 'row', height: 1 }}>
|
|
584
|
+
<text fg="#06b6d4" style={{ attributes: TextAttributes.BOLD }}>{String(agent.name.toUpperCase())} </text>
|
|
585
|
+
<text fg="#ffffff">{String(getModelDisplayName(config.model || ''))} </text>
|
|
586
|
+
<text fg="#666666">{String(getProviderDisplayName(config.model || ''))}</text>
|
|
587
|
+
{accountStats.total > 0 ? <text fg="#666666"> · </text> : null}
|
|
588
|
+
{accountStats.total > 0 ? <text fg="#eab308">{String(accountStats.active)}</text> : null}
|
|
589
|
+
{accountStats.total > 0 ? <text fg="#666666"> ({String(accountStats.count)}/{String(accountStats.total)})</text> : null}
|
|
590
|
+
{skills.length > 0 ? <text fg="#666666"> · Skills: {String(skills.length)}</text> : null}
|
|
591
|
+
{subAgentCount > 0 ? <text fg="#666666"> · Sub-agents: {String(subAgentCount)}</text> : null}
|
|
592
|
+
</box>
|
|
593
|
+
</box>
|
|
594
|
+
<box style={{ flexDirection: 'row', justifyContent: 'flex-end', marginRight: 2, marginBottom: 1, height: 1 }}>
|
|
595
|
+
<box style={{ flexDirection: 'row' }}>
|
|
596
|
+
<text fg="#ffffff" style={{ attributes: TextAttributes.BOLD }}>ctrl+t</text><text fg="#666666"> variants </text>
|
|
597
|
+
<text fg="#ffffff" style={{ attributes: TextAttributes.BOLD }}>tab</text><text fg="#666666"> agents </text>
|
|
598
|
+
<text fg="#ffffff" style={{ attributes: TextAttributes.BOLD }}>ctrl+p</text><text fg="#666666"> commands</text>
|
|
599
|
+
</box>
|
|
600
|
+
</box>
|
|
601
|
+
{mode === 'commands' ? (
|
|
602
|
+
<Modal title="Commands" width={width} height={height} onClose={() => { setMode('chat'); setSearchValue(''); }}>
|
|
603
|
+
<scrollbox style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1 }} focused={mode === 'commands'}>
|
|
604
|
+
{filteredCommands.map((cmd, i) => <CommandItem key={cmd.id} name={cmd.name} shortcut={cmd.shortcut} isSelected={i === selectionIndex} onHover={() => setSelectionIndex(i)} onClick={cmd.action} />)}
|
|
605
|
+
</scrollbox>
|
|
606
|
+
</Modal>
|
|
607
|
+
) : null}
|
|
608
|
+
{mode === 'models' ? (
|
|
609
|
+
<Modal title="Models" width={width} height={height} onClose={() => setMode('chat')}>
|
|
610
|
+
<scrollbox style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1 }} focused>
|
|
611
|
+
{availableModels.map((m, i) => <CommandItem key={m.id} name={m.name} isSelected={i === selectionIndex} onHover={() => setSelectionIndex(i)} onClick={() => { if(config){ config.model = m.id; setConfig({...config}); setMode('chat'); } }} />)}
|
|
612
|
+
</scrollbox>
|
|
613
|
+
</Modal>
|
|
614
|
+
) : null}
|
|
615
|
+
{mode === 'variants' ? (
|
|
616
|
+
<Modal title="Variants" width={width} height={height} onClose={() => setMode('chat')}>
|
|
617
|
+
<scrollbox style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1 }} focused>
|
|
618
|
+
{currentVariants.map((v, i) => <CommandItem key={v} name={v} isSelected={i === selectionIndex} onHover={() => setSelectionIndex(i)} onClick={() => { if(v) { setVariant(v); setMode('chat'); } }} />)}
|
|
619
|
+
</scrollbox>
|
|
620
|
+
</Modal>
|
|
621
|
+
) : null}
|
|
622
|
+
{mode === 'providers' ? (
|
|
623
|
+
<Modal title="Providers" width={width} height={height} onClose={() => setMode('chat')}>
|
|
624
|
+
<scrollbox style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1 }} focused>
|
|
625
|
+
{providersList.map((p, i) => (
|
|
626
|
+
<CommandItem
|
|
627
|
+
key={p.id}
|
|
628
|
+
name={p.name}
|
|
629
|
+
description={p.isAvailable ? 'Connected' : `Missing ${p.envVar}`}
|
|
630
|
+
isSelected={i === selectionIndex}
|
|
631
|
+
onHover={() => setSelectionIndex(i)}
|
|
632
|
+
onClick={() => setMode('chat')}
|
|
633
|
+
/>
|
|
634
|
+
))}
|
|
635
|
+
</scrollbox>
|
|
636
|
+
</Modal>
|
|
637
|
+
) : null}
|
|
638
|
+
{mode === 'settings' ? (
|
|
639
|
+
<Modal title="Privacy & Security" width={width} height={height} onClose={() => setMode('chat')}>
|
|
640
|
+
<scrollbox style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1 }} focused>
|
|
641
|
+
{settingsList.map((s, i) => (
|
|
642
|
+
<CommandItem
|
|
643
|
+
key={s.id}
|
|
644
|
+
name={s.name}
|
|
645
|
+
description={String((config?.permission as any)?.[s.id] || 'ask')}
|
|
646
|
+
isSelected={i === selectionIndex}
|
|
647
|
+
onHover={() => setSelectionIndex(i)}
|
|
648
|
+
onClick={() => togglePermission(s.id)}
|
|
649
|
+
/>
|
|
650
|
+
))}
|
|
651
|
+
</scrollbox>
|
|
652
|
+
</Modal>
|
|
653
|
+
) : null}
|
|
654
|
+
{mode === 'auth' ? (
|
|
655
|
+
<Modal title="Connect Account" width={width} height={height} onClose={() => { setMode('chat'); setAuthStep('list'); }}>
|
|
656
|
+
{authStep === 'list' ? (
|
|
657
|
+
<scrollbox style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1 }} focused>
|
|
658
|
+
{authProviders.map((p, i) => (
|
|
659
|
+
<CommandItem
|
|
660
|
+
key={p.id}
|
|
661
|
+
name={p.name + (p.id === 'google' ? ' (Recommended)' : '')}
|
|
662
|
+
description={p.description}
|
|
663
|
+
isSelected={i === selectionIndex}
|
|
664
|
+
onHover={() => setSelectionIndex(i)}
|
|
665
|
+
onClick={() => {
|
|
666
|
+
setSelectedProviderId(p.id);
|
|
667
|
+
if (p.id === 'google') {
|
|
668
|
+
setAuthStep('method');
|
|
669
|
+
setSelectionIndex(0);
|
|
670
|
+
} else {
|
|
671
|
+
setAuthStep('apikey');
|
|
672
|
+
setInputValue('');
|
|
673
|
+
}
|
|
674
|
+
}}
|
|
675
|
+
/>
|
|
676
|
+
))}
|
|
677
|
+
</scrollbox>
|
|
678
|
+
) : authStep === 'method' ? (
|
|
679
|
+
<scrollbox style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1 }} focused>
|
|
680
|
+
<CommandItem
|
|
681
|
+
name="OAuth with Google (Recommended)"
|
|
682
|
+
description="Uses Google account, no API key needed"
|
|
683
|
+
isSelected={selectionIndex === 0}
|
|
684
|
+
onHover={() => setSelectionIndex(0)}
|
|
685
|
+
onClick={() => { setMode('chat'); startAntigravityLogin().catch(e => setError(e.message)); }}
|
|
686
|
+
/>
|
|
687
|
+
<CommandItem
|
|
688
|
+
name="Enter API Key"
|
|
689
|
+
description="Use your own Google AI Studio API key"
|
|
690
|
+
isSelected={selectionIndex === 1}
|
|
691
|
+
onHover={() => setSelectionIndex(1)}
|
|
692
|
+
onClick={() => { setAuthStep('apikey'); setInputValue(''); }}
|
|
693
|
+
/>
|
|
694
|
+
</scrollbox>
|
|
695
|
+
) : (
|
|
696
|
+
<box style={{ padding: 1, flexDirection: 'column' }}>
|
|
697
|
+
<text fg="#ffffff">Enter your API key below for {String(selectedProviderId)}:</text>
|
|
698
|
+
<text fg="#666666">Press Enter to save.</text>
|
|
699
|
+
</box>
|
|
700
|
+
)}
|
|
701
|
+
</Modal>
|
|
702
|
+
) : null}
|
|
703
|
+
{mode === 'skills' ? (
|
|
704
|
+
<Modal title="Skills" width={width} height={height} onClose={() => setMode('chat')}>
|
|
705
|
+
{skillStep === 'list' ? (
|
|
706
|
+
<scrollbox style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1 }} focused>
|
|
707
|
+
<CommandItem
|
|
708
|
+
name="+ Create New Skill"
|
|
709
|
+
description="Add a new capability"
|
|
710
|
+
isSelected={selectionIndex === 0}
|
|
711
|
+
onHover={() => setSelectionIndex(0)}
|
|
712
|
+
onClick={() => {
|
|
713
|
+
setSkillStep('create');
|
|
714
|
+
setNewSkillName('');
|
|
715
|
+
setNewSkillDesc('');
|
|
716
|
+
setInputValue('');
|
|
717
|
+
}}
|
|
718
|
+
/>
|
|
719
|
+
{skills.map((s, i) => (
|
|
720
|
+
<CommandItem
|
|
721
|
+
key={s.name}
|
|
722
|
+
name={s.name}
|
|
723
|
+
description={s.description.slice(0, 50)}
|
|
724
|
+
isSelected={i + 1 === selectionIndex}
|
|
725
|
+
onHover={() => setSelectionIndex(i + 1)}
|
|
726
|
+
onClick={() => {
|
|
727
|
+
setInputValue((prev) => prev + (prev ? '\n' : '') + `Use skill: ${s.name}`);
|
|
728
|
+
setMode('chat');
|
|
729
|
+
}}
|
|
730
|
+
/>
|
|
731
|
+
))}
|
|
732
|
+
</scrollbox>
|
|
733
|
+
) : (
|
|
734
|
+
<box style={{ padding: 1, flexDirection: 'column' }}>
|
|
735
|
+
<text fg="#ffffff">{!newSkillName ? 'Enter Skill Name:' : 'Enter Skill Description:'}</text>
|
|
736
|
+
<text fg="#666666">Type in the main input box below and press Enter.</text>
|
|
737
|
+
{newSkillName ? <text fg="#06b6d4" style={{ marginTop: 1 }}>Name: {String(newSkillName)}</text> : null}
|
|
738
|
+
</box>
|
|
739
|
+
)}
|
|
740
|
+
</Modal>
|
|
741
|
+
) : null}
|
|
742
|
+
{shutdownState !== 'none' ? (
|
|
743
|
+
<box style={{ position: 'absolute', top: 0, left: 0, width, height, alignItems: 'center', justifyContent: 'center', backgroundColor: '#000000', zIndex: 999, flexDirection: 'column' }}>
|
|
744
|
+
{shutdownState === 'summary' ? (
|
|
745
|
+
<box style={{ flexDirection: 'column', alignItems: 'center', borderStyle: 'double', borderColor: '#06b6d4', padding: 2 }}>
|
|
746
|
+
<text fg="#06b6d4" style={{ attributes: TextAttributes.BOLD }}>SESSION COMPLETE</text>
|
|
747
|
+
<box style={{ height: 1 }} />
|
|
748
|
+
<text fg="#ffffff">Duration: {String(Math.floor((Date.now() - appStartTime.current) / 1000))}s</text>
|
|
749
|
+
<text fg="#ffffff">Tokens: {String(totalTokens.toLocaleString())}</text>
|
|
750
|
+
<text fg="#ffffff">Cost: ${String(costEstimate.toFixed(4))}</text>
|
|
751
|
+
<box style={{ height: 1 }} />
|
|
752
|
+
<text fg="#666666">Have a good evening.</text>
|
|
753
|
+
</box>
|
|
754
|
+
) : (
|
|
755
|
+
<text fg="#06b6d4">Powering down...</text>
|
|
756
|
+
)}
|
|
757
|
+
</box>
|
|
758
|
+
) : null}
|
|
759
|
+
</box>
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export async function startTui(props: AppProps) {
|
|
764
|
+
const renderer = await createCliRenderer({
|
|
765
|
+
useMouse: true,
|
|
766
|
+
enableMouseMovement: true,
|
|
767
|
+
onDestroy: () => {
|
|
768
|
+
process.stdout.write('\u001b[?25h'); // Show cursor
|
|
769
|
+
process.stdout.write('\u001b[2J\u001b[3J\u001b[H'); // Clear screen
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const runInteractive = async (task: () => Promise<void>) => {
|
|
774
|
+
renderer.suspend();
|
|
775
|
+
process.stdout.write('\u001b[2J\u001b[3J\u001b[H');
|
|
776
|
+
try {
|
|
777
|
+
await task();
|
|
778
|
+
} finally {
|
|
779
|
+
renderer.resume();
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
createRoot(renderer).render(<App {...props} runInteractive={runInteractive} />);
|
|
784
|
+
}
|