glitool 1.0.0 → 2.0.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/README.md +115 -45
- package/dist/agent.js +234 -37
- package/dist/agents/coder.js +46 -34
- package/dist/agents/debugger.js +111 -0
- package/dist/agents/explainer.js +2 -5
- package/dist/agents/git-agent.js +90 -0
- package/dist/agents/graph.js +214 -23
- package/dist/agents/judge.js +61 -0
- package/dist/agents/planner.js +31 -10
- package/dist/agents/planningAgent.js +41 -0
- package/dist/agents/refactorer.js +97 -0
- package/dist/agents/reviewer-agent.js +87 -0
- package/dist/agents/reviewer.js +6 -9
- package/dist/agents/types.js +1 -0
- package/dist/agents/validator.js +93 -0
- package/dist/agents/workflow.js +45 -0
- package/dist/auth.js +87 -0
- package/dist/commands/version.js +1 -0
- package/dist/config.js +4 -1
- package/dist/confirmHandler.js +4 -2
- package/dist/index.js +12 -25
- package/dist/llm/classifier.js +61 -0
- package/dist/llm/factory.js +50 -0
- package/dist/llm/router.js +235 -14
- package/dist/llm/telemetry.js +18 -0
- package/dist/logger.js +25 -0
- package/dist/processEvents.js +1 -0
- package/dist/tools/bashTool.js +90 -0
- package/dist/tools/editFileTool.js +14 -3
- package/dist/tools/index.js +3 -1
- package/dist/tools/listFilesTool.js +19 -21
- package/dist/tools/processRegistry.js +36 -0
- package/dist/tools/readBackgroundOutput.js +29 -0
- package/dist/tools/readFileTool.js +64 -9
- package/dist/tools/searchCodeTool.js +14 -4
- package/dist/tools/webFetchTool.js +45 -0
- package/dist/tools/writeFileTool.js +9 -6
- package/dist/trust/riskScorer.js +29 -2
- package/dist/ui/App.js +384 -47
- package/dist/ui/AuthFlow.js +76 -0
- package/dist/ui/ConfirmCard.js +53 -0
- package/dist/ui/EscalationCard.js +22 -0
- package/dist/ui/ExplainCard.js +5 -0
- package/dist/ui/Pipeline.js +37 -0
- package/dist/ui/ProcessTrace.js +79 -0
- package/dist/ui/RoleRow.js +16 -0
- package/dist/ui/RoleRow.test.js +8 -0
- package/dist/ui/SlashPalette.js +32 -0
- package/dist/ui/StatusBar.js +44 -0
- package/dist/ui/ToolLog.js +62 -0
- package/dist/ui/Welcome.js +11 -0
- package/dist/ui/renderMarkdown.js +41 -0
- package/dist/ui/symbols.js +19 -0
- package/dist/ui/tokens.js +13 -0
- package/dist/version.js +1 -0
- package/package.json +27 -20
package/dist/ui/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useState,
|
|
3
|
-
import { Box, Text, useApp } from 'ink';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useRef } from "react";
|
|
3
|
+
import { Box, Text, useApp, Static } from 'ink';
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import { chat, clearSession, llm, sessionMessages } from '../agent.js';
|
|
6
6
|
import { clearSummary, generateAndSaveSummary } from "../memory.js";
|
|
@@ -8,40 +8,181 @@ import { clearProjectMemory, extractAndSaveProjectMemory } from "../projectMemor
|
|
|
8
8
|
import { useInput } from 'ink';
|
|
9
9
|
import { setConfirmHandler } from "../confirmHandler.js";
|
|
10
10
|
import { explainResponse } from "../agents/explainer.js";
|
|
11
|
-
|
|
11
|
+
import { loadConfig } from '../config.js';
|
|
12
|
+
import { Welcome } from './Welcome.js';
|
|
13
|
+
import { StatusBar } from './StatusBar.js';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
import { SlashPalette, filterCommands } from './SlashPalette.js';
|
|
16
|
+
import { ToolLog } from "./ToolLog.js";
|
|
17
|
+
import { Pipeline } from "./Pipeline.js";
|
|
18
|
+
import { ConfirmCard } from './ConfirmCard.js';
|
|
19
|
+
import { ExplainCard } from "./ExplainCard.js";
|
|
20
|
+
import { colors } from "./tokens.js";
|
|
21
|
+
import { EscalationCard } from './EscalationCard.js';
|
|
22
|
+
import { log } from "../logger.js";
|
|
23
|
+
import { renderMarkdown } from "./renderMarkdown.js";
|
|
24
|
+
import { ProcessTrace } from './ProcessTrace.js';
|
|
25
|
+
import { AuthFlow } from './AuthFlow.js';
|
|
26
|
+
import { getAnonRequestCount, incrementAnonCount, isAnonLimitReached, isAuthenticated, readAuth, ANON_LIMIT, } from '../auth.js';
|
|
27
|
+
// const previousInputRef = useRef('');
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
function getWorkspaceStats() {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
let branch = 'main';
|
|
32
|
+
let files = 0;
|
|
33
|
+
let loc = '';
|
|
34
|
+
try {
|
|
35
|
+
branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
try {
|
|
39
|
+
files = parseInt(execSync('git ls-files | wc -l', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(), 10);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
try {
|
|
43
|
+
const raw = execSync('git ls-files | xargs wc -l 2>/dev/null | tail -1', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
44
|
+
const n = parseInt(raw, 10);
|
|
45
|
+
if (!isNaN(n))
|
|
46
|
+
loc = `${(n / 1000).toFixed(1)}k LOC`;
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
return {
|
|
50
|
+
path: cwd.replace(process.env.HOME ?? '', '~'),
|
|
51
|
+
branch,
|
|
52
|
+
files,
|
|
53
|
+
loc,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const workspaceStats = getWorkspaceStats();
|
|
12
57
|
export const App = ({ explainMode = false }) => {
|
|
13
58
|
const { exit } = useApp();
|
|
59
|
+
const auth = readAuth();
|
|
60
|
+
const [anonCount, setAnonCount] = useState(getAnonRequestCount());
|
|
61
|
+
const isByok = !!process.env.OPENAI_API_KEY;
|
|
62
|
+
const anonLeft = (isAuthenticated() || isByok) ? undefined : Math.max(0, ANON_LIMIT - anonCount);
|
|
14
63
|
const [messages, setMessages] = useState([]);
|
|
15
64
|
const [input, setInput] = useState('');
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const [
|
|
20
|
-
const [confirmInput, setConfirmInput] = useState('');
|
|
65
|
+
const previousInputRef = useRef('');
|
|
66
|
+
const ctrlVHandledRef = useRef(false);
|
|
67
|
+
// ← ADD HERE
|
|
68
|
+
const [pastedContent, setPastedContent] = useState('');
|
|
21
69
|
const [confirmResolver, setConfirmResolver] = useState(null);
|
|
22
70
|
const [streamingContent, setStreamingContent] = useState('');
|
|
23
71
|
const [inputHistory, setInputHistory] = useState([]);
|
|
24
72
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
73
|
+
const [statusState, setStatusState] = useState('idle');
|
|
74
|
+
const [statusDetail, setStatusDetail] = useState('');
|
|
75
|
+
const [tokens, setTokens] = useState(0);
|
|
76
|
+
const [cost, setCost] = useState(0);
|
|
77
|
+
const [confirmRequest, setConfirmRequest] = useState(null);
|
|
78
|
+
const [toolLog, setToolLog] = useState([]);
|
|
79
|
+
const [stageEvents, setStageEvents] = useState([]);
|
|
80
|
+
const [planner, setPlanner] = useState({ status: 'idle' });
|
|
81
|
+
const [coder, setCoder] = useState({ status: 'idle' });
|
|
82
|
+
const [workflow, setWorkflow] = useState({ status: 'idle' });
|
|
83
|
+
const [validator, setValidator] = useState({ status: 'idle' });
|
|
84
|
+
const [escalation, setEscalation] = useState(null);
|
|
85
|
+
const [judge, setJudge] = useState({ status: 'idle' });
|
|
86
|
+
const [inputKey, setInputKey] = useState(0);
|
|
87
|
+
const [showAuth, setShowAuth] = useState(false);
|
|
88
|
+
const [paletteIndex, setPaletteIndex] = useState(0);
|
|
89
|
+
const paletteItems = filterCommands(input);
|
|
25
90
|
const handleChange = (value) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
91
|
+
if (ctrlVHandledRef.current) {
|
|
92
|
+
ctrlVHandledRef.current = false;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const previous = previousInputRef.current;
|
|
96
|
+
const grew = value.length - previous.length;
|
|
97
|
+
const grewALot = grew > 20;
|
|
98
|
+
const newlineAppeared = /[\r\n]/.test(value) && !/[\r\n]/.test(previous);
|
|
99
|
+
if (grewALot || newlineAppeared) {
|
|
100
|
+
let pasted;
|
|
101
|
+
if (value.startsWith(previous)) {
|
|
102
|
+
pasted = value.slice(previous.length);
|
|
103
|
+
}
|
|
104
|
+
else if (value.endsWith(previous)) {
|
|
105
|
+
pasted = value.slice(0, value.length - previous.length);
|
|
106
|
+
}
|
|
107
|
+
else if (previous && value.includes(previous)) {
|
|
108
|
+
pasted = value.replace(previous, '');
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
pasted = value;
|
|
112
|
+
}
|
|
113
|
+
if (pasted.includes('\n')) {
|
|
114
|
+
// multi-line → clipping section
|
|
115
|
+
setPastedContent(prev => (prev ? `${prev}\n\n${pasted}` : pasted));
|
|
116
|
+
setInput(previous);
|
|
117
|
+
previousInputRef.current = previous;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// single line → stays in input field
|
|
121
|
+
setInput(value);
|
|
122
|
+
previousInputRef.current = value;
|
|
123
|
+
}
|
|
30
124
|
}
|
|
31
125
|
else {
|
|
32
|
-
|
|
126
|
+
setInput(value);
|
|
127
|
+
previousInputRef.current = value;
|
|
33
128
|
}
|
|
129
|
+
setPaletteIndex(0);
|
|
34
130
|
};
|
|
35
131
|
const handleExit = async () => {
|
|
36
132
|
await generateAndSaveSummary(sessionMessages, llm);
|
|
37
133
|
await extractAndSaveProjectMemory(sessionMessages, llm);
|
|
38
134
|
exit();
|
|
39
135
|
};
|
|
40
|
-
useInput((
|
|
41
|
-
if (
|
|
136
|
+
useInput((inputKey, key) => {
|
|
137
|
+
if (confirmRequest)
|
|
138
|
+
return;
|
|
139
|
+
if (escalation)
|
|
140
|
+
return;
|
|
141
|
+
if (statusState === 'working')
|
|
142
|
+
return;
|
|
143
|
+
// Ctrl+V — read clipboard directly
|
|
144
|
+
if (inputKey === '\x16' || (key.ctrl && inputKey === 'v')) {
|
|
145
|
+
try {
|
|
146
|
+
ctrlVHandledRef.current = true;
|
|
147
|
+
const text = execSync('xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output 2>/dev/null', { encoding: 'utf8', timeout: 2000 }).trimEnd();
|
|
148
|
+
if (!text)
|
|
149
|
+
return;
|
|
150
|
+
if (text.includes('\n')) {
|
|
151
|
+
// multi-line → clipping section
|
|
152
|
+
setPastedContent(prev => prev ? `${prev}\n\n${text}` : text);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// single line → goes straight into input field
|
|
156
|
+
setInput(prev => prev + text);
|
|
157
|
+
previousInputRef.current = previousInputRef.current + text;
|
|
158
|
+
setInputKey(k => k + 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch { }
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.escape && showAuth) {
|
|
165
|
+
setShowAuth(false);
|
|
42
166
|
return;
|
|
43
|
-
|
|
167
|
+
}
|
|
168
|
+
if (key.escape && pastedContent) {
|
|
169
|
+
setPastedContent('');
|
|
44
170
|
return;
|
|
171
|
+
}
|
|
172
|
+
if (paletteItems.length > 0) {
|
|
173
|
+
if (key.upArrow) {
|
|
174
|
+
setPaletteIndex(i => (i - 1 + paletteItems.length) % paletteItems.length);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (key.downArrow) {
|
|
178
|
+
setPaletteIndex(i => (i + 1) % paletteItems.length);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (key.escape) {
|
|
182
|
+
setInput('');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
45
186
|
if (key.upArrow) {
|
|
46
187
|
if (inputHistory.length === 0)
|
|
47
188
|
return;
|
|
@@ -59,27 +200,45 @@ export const App = ({ explainMode = false }) => {
|
|
|
59
200
|
setHistoryIndex(newIndex);
|
|
60
201
|
setInput(inputHistory[inputHistory.length - 1 - newIndex] ?? '');
|
|
61
202
|
}
|
|
62
|
-
if (key.tab && suggestions.length > 0) {
|
|
63
|
-
setInput(suggestions[0] ?? '');
|
|
64
|
-
setSuggestions([]);
|
|
65
|
-
}
|
|
66
203
|
});
|
|
204
|
+
function pastePreview(content) {
|
|
205
|
+
const lines = content.split('\n');
|
|
206
|
+
const sizeKB = (content.length / 1024).toFixed(1);
|
|
207
|
+
const lineCount = lines.length;
|
|
208
|
+
const headline = `${lineCount} line${lineCount === 1 ? '' : 's'} · ${sizeKB} KB`;
|
|
209
|
+
const firstNonEmpty = lines.find(l => l.trim()) ?? '';
|
|
210
|
+
const truncated = firstNonEmpty.length > 80 ? firstNonEmpty.slice(0, 80) + '…' : firstNonEmpty;
|
|
211
|
+
const moreNote = lineCount > 1 ? ` · +${lineCount - 1} more line${lineCount - 1 === 1 ? '' : 's'}` : '';
|
|
212
|
+
return { headline, body: `${truncated}${moreNote}` };
|
|
213
|
+
}
|
|
67
214
|
React.useEffect(() => {
|
|
68
|
-
setConfirmHandler((
|
|
215
|
+
setConfirmHandler((req) => {
|
|
69
216
|
return new Promise((resolve) => {
|
|
70
|
-
|
|
217
|
+
setConfirmRequest(req);
|
|
218
|
+
setStatusState('awaiting');
|
|
71
219
|
setConfirmResolver(() => resolve);
|
|
220
|
+
log('confirm:resolver-set', { filePath: req.filePath });
|
|
72
221
|
});
|
|
73
222
|
});
|
|
74
223
|
}, []);
|
|
75
|
-
const handleSubmit =
|
|
76
|
-
|
|
224
|
+
const handleSubmit = async (value) => {
|
|
225
|
+
let cmd = value.trim();
|
|
226
|
+
if (pastedContent) {
|
|
227
|
+
cmd = cmd ? `${pastedContent}\n\n${cmd}` : pastedContent;
|
|
228
|
+
setPastedContent('');
|
|
229
|
+
}
|
|
77
230
|
setInput('');
|
|
78
|
-
setSuggestions([]);
|
|
79
231
|
if (!cmd)
|
|
80
232
|
return;
|
|
81
233
|
setInputHistory(prev => prev[prev.length - 1] === cmd ? prev : [...prev, cmd]);
|
|
82
234
|
setHistoryIndex(-1);
|
|
235
|
+
if (paletteItems.length > 0 && value.startsWith('/')) {
|
|
236
|
+
const selected = paletteItems[paletteIndex];
|
|
237
|
+
if (selected && selected.cmd !== value) {
|
|
238
|
+
setInput(selected.cmd);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
83
242
|
if (cmd === '/exit') {
|
|
84
243
|
await handleExit();
|
|
85
244
|
return;
|
|
@@ -99,33 +258,159 @@ export const App = ({ explainMode = false }) => {
|
|
|
99
258
|
if (cmd === '/help') {
|
|
100
259
|
setMessages(prev => [...prev, {
|
|
101
260
|
role: 'assistant',
|
|
102
|
-
content:
|
|
261
|
+
content: [
|
|
262
|
+
'Slash commands:',
|
|
263
|
+
' /plan — create or update plan.md',
|
|
264
|
+
' /coder — run the full coding pipeline',
|
|
265
|
+
' /debug — investigate and fix a bug',
|
|
266
|
+
' /refactor — structural code improvements',
|
|
267
|
+
' /review — read-only code analysis',
|
|
268
|
+
' /git — git operations',
|
|
269
|
+
' /explain — explain a concept or file',
|
|
270
|
+
' /quick — fast chat, no tools',
|
|
271
|
+
'',
|
|
272
|
+
'Session commands:',
|
|
273
|
+
' /clear — clear conversation (keep memory)',
|
|
274
|
+
' /reset — clear conversation + memory',
|
|
275
|
+
' /model — show current model',
|
|
276
|
+
' /tools — list available tools',
|
|
277
|
+
' /memory — show project memory',
|
|
278
|
+
' /exit — save session and exit',
|
|
279
|
+
' /signup — sign in with GitHub (50 req/month free)',
|
|
280
|
+
].join('\n')
|
|
103
281
|
}]);
|
|
104
282
|
return;
|
|
105
283
|
}
|
|
106
284
|
if (cmd === '/model') {
|
|
107
|
-
setMessages(prev => [...prev, { role: 'assistant', content:
|
|
285
|
+
setMessages(prev => [...prev, { role: 'assistant', content: `Model: ${config.preferredModel ?? 'gpt-4o-mini'}` }]);
|
|
108
286
|
return;
|
|
109
287
|
}
|
|
110
288
|
if (cmd === '/tools') {
|
|
111
289
|
setMessages(prev => [...prev, {
|
|
112
290
|
role: 'assistant',
|
|
113
|
-
content:
|
|
291
|
+
content: [
|
|
292
|
+
'Available tools:',
|
|
293
|
+
' listFiles — list project files (supports glob patterns)',
|
|
294
|
+
' readFile — read a file by name or path',
|
|
295
|
+
' searchCode — grep for symbols, functions, keywords',
|
|
296
|
+
' editFile — make targeted edits to existing files',
|
|
297
|
+
' writeFile — create or overwrite a file',
|
|
298
|
+
' bash — run shell commands (risk-gated)',
|
|
299
|
+
' webFetch — fetch a URL and return markdown content',
|
|
300
|
+
' readBackground — read output from a background process',
|
|
301
|
+
].join('\n')
|
|
114
302
|
}]);
|
|
115
303
|
return;
|
|
116
304
|
}
|
|
305
|
+
if (cmd === '/memory') {
|
|
306
|
+
const { loadProjectMemory } = await import('../projectMemory.js');
|
|
307
|
+
const mem = loadProjectMemory();
|
|
308
|
+
const content = mem
|
|
309
|
+
? [
|
|
310
|
+
`Tech stack: ${mem.techStack?.join(', ') ?? 'unknown'}`,
|
|
311
|
+
mem.architectureDecisions?.length ? `Architecture: ${mem.architectureDecisions.join(' · ')}` : '',
|
|
312
|
+
mem.todos?.length ? `TODOs: ${mem.todos.join(' · ')}` : '',
|
|
313
|
+
].filter(Boolean).join('\n')
|
|
314
|
+
: 'No project memory recorded yet. Keep chatting — it builds automatically on exit.';
|
|
315
|
+
setMessages(prev => [...prev, { role: 'assistant', content }]);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (cmd === '/signup') {
|
|
319
|
+
setShowAuth(true);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
117
322
|
setMessages(prev => [...prev, { role: 'user', content: cmd }]);
|
|
118
|
-
|
|
323
|
+
setStatusState('working');
|
|
324
|
+
setToolLog([]);
|
|
325
|
+
setStageEvents([]);
|
|
326
|
+
setPlanner({ status: 'idle' });
|
|
327
|
+
setWorkflow({ status: 'idle' });
|
|
328
|
+
setCoder({ status: 'idle' });
|
|
329
|
+
setValidator({ status: 'idle' });
|
|
330
|
+
setJudge({ status: 'idle' });
|
|
331
|
+
if (!isAuthenticated() && !process.env.OPENAI_API_KEY && isAnonLimitReached()) {
|
|
332
|
+
setMessages(prev => [...prev, {
|
|
333
|
+
role: 'assistant',
|
|
334
|
+
content: [
|
|
335
|
+
"You've used your 5 free requests.",
|
|
336
|
+
'',
|
|
337
|
+
'Sign in with GitHub — free, 50 requests/month:',
|
|
338
|
+
' → https://glitool.dev/activate',
|
|
339
|
+
'',
|
|
340
|
+
'Type /signup to start the sign-in flow in your terminal.',
|
|
341
|
+
].join('\n'),
|
|
342
|
+
}]);
|
|
343
|
+
setStatusState('idle');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
119
346
|
try {
|
|
120
347
|
const reply = await chat(cmd, (toolName, args) => {
|
|
121
|
-
|
|
122
|
-
|
|
348
|
+
let argStr = '';
|
|
349
|
+
if (args) {
|
|
350
|
+
const first = Object.values(args)[0];
|
|
351
|
+
if (typeof first === 'string') {
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(first);
|
|
354
|
+
argStr = parsed.command ?? parsed.filePath ?? parsed.pattern ?? parsed.query ?? parsed.url ?? first;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
argStr = first;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (typeof first === 'object' && first !== null) {
|
|
361
|
+
argStr = first.command ?? first.filePath ?? JSON.stringify(first).slice(0, 60);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
argStr = String(first ?? '');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
setToolLog(prev => {
|
|
368
|
+
const updated = prev.map(e => e.status === 'running' ? { ...e, status: 'done' } : e);
|
|
369
|
+
return [...updated, { tool: toolName, target: argStr, status: 'running' }];
|
|
370
|
+
});
|
|
123
371
|
}, (status) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
372
|
+
if (status.startsWith('Planning')) {
|
|
373
|
+
setPlanner({ status: 'active', detail: 'planning steps' });
|
|
374
|
+
setWorkflow({ status: 'queued' });
|
|
375
|
+
setCoder({ status: 'queued' });
|
|
376
|
+
setValidator({ status: 'queued' });
|
|
377
|
+
setJudge({ status: 'queued' });
|
|
378
|
+
}
|
|
379
|
+
else if (status.startsWith('Building execution') || status.startsWith('Workflow')) {
|
|
380
|
+
setPlanner({ status: 'done' });
|
|
381
|
+
setWorkflow({ status: 'active', detail: 'building DAG' });
|
|
382
|
+
}
|
|
383
|
+
else if (status.startsWith('Executing')) {
|
|
384
|
+
setWorkflow({ status: 'done' });
|
|
385
|
+
setCoder({ status: 'active', detail: 'editing files' });
|
|
386
|
+
}
|
|
387
|
+
else if (status.startsWith('Validating')) {
|
|
388
|
+
setCoder({ status: 'done' });
|
|
389
|
+
setValidator({ status: 'active', detail: 'tsc + eslint' });
|
|
390
|
+
}
|
|
391
|
+
else if (status.startsWith('Validation failed')) {
|
|
392
|
+
setValidator({ status: 'active', detail: status.replace('Validation failed', '').trim() });
|
|
393
|
+
}
|
|
394
|
+
else if (status.startsWith('Judging') || status.startsWith('Judge:')) {
|
|
395
|
+
setValidator({ status: 'done' });
|
|
396
|
+
setJudge({ status: 'active', detail: 'reviewing output' });
|
|
397
|
+
}
|
|
398
|
+
else if (status.startsWith('Escalating')) {
|
|
399
|
+
setJudge({ status: 'active', detail: 'escalating' });
|
|
400
|
+
}
|
|
401
|
+
}, (token) => setStreamingContent(prev => prev + token), (payload) => setEscalation(payload), (newTokens, newCost) => {
|
|
402
|
+
setTokens(prev => prev + newTokens);
|
|
403
|
+
setCost(prev => prev + newCost);
|
|
404
|
+
}, (event) => setStageEvents(prev => [...prev, event]));
|
|
405
|
+
if (!isAuthenticated() && !process.env.OPENAI_API_KEY) {
|
|
406
|
+
const newCount = incrementAnonCount();
|
|
407
|
+
setAnonCount(newCount);
|
|
408
|
+
}
|
|
128
409
|
setStreamingContent('');
|
|
410
|
+
if (stageEvents.length > 0) {
|
|
411
|
+
setMessages(prev => [...prev, { role: 'trace', content: '', traceEvents: [...stageEvents] }]);
|
|
412
|
+
}
|
|
413
|
+
setStageEvents([]);
|
|
129
414
|
setMessages(prev => [...prev, { role: 'assistant', content: reply }]);
|
|
130
415
|
if (explainMode && reply) {
|
|
131
416
|
const explanation = await explainResponse(reply);
|
|
@@ -141,16 +426,68 @@ export const App = ({ explainMode = false }) => {
|
|
|
141
426
|
}]);
|
|
142
427
|
}
|
|
143
428
|
finally {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
429
|
+
setStageEvents([]); // ← add this
|
|
430
|
+
setToolLog(prev => prev.map(e => e.status === 'running' ? { ...e, status: 'done' } : e));
|
|
431
|
+
setPlanner(p => p.status === 'active' ? { status: 'done' } : p);
|
|
432
|
+
setWorkflow(w => w.status === 'active' ? { status: 'done' } : w);
|
|
433
|
+
setCoder(c => c.status === 'active' ? { status: 'done' } : c);
|
|
434
|
+
setValidator(v => v.status === 'active' ? { status: 'done' } : v);
|
|
435
|
+
setJudge(j => j.status === 'active' ? { status: 'done' } : j);
|
|
436
|
+
setStreamingContent('');
|
|
437
|
+
setStatusState('idle');
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: [1], children: (_, i) => (_jsx(Welcome, { name: config.name, version: "1.0.1", workspace: workspaceStats, runtime: {
|
|
441
|
+
model: config.preferredModel,
|
|
442
|
+
toolsCount: 6,
|
|
443
|
+
explainOn: explainMode,
|
|
444
|
+
routerOn: true,
|
|
445
|
+
} }, i)) }), _jsxs(Box, { flexDirection: "column", padding: 1, children: [messages.map((msg, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [msg.role === 'user' && (_jsxs(Box, { borderStyle: "round", borderColor: "white", paddingX: 1, children: [_jsx(Text, { bold: true, color: "white", children: "You" }), _jsx(Text, { wrap: "wrap", children: msg.content })] })), msg.role === 'trace' && msg.traceEvents && (_jsx(Box, { marginLeft: 1, marginBottom: 1, children: _jsx(ProcessTrace, { events: msg.traceEvents, active: false }) })), msg.role === 'assistant' && (() => {
|
|
446
|
+
const rendered = renderMarkdown(msg.content);
|
|
447
|
+
const isLong = msg.content.split('\n').length > 6;
|
|
448
|
+
if (isLong) {
|
|
449
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Assistant" }), _jsx(Text, { children: rendered })] }));
|
|
450
|
+
}
|
|
451
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: " Assistant " }), _jsxs(Text, { children: [" ", rendered, " "] })] }));
|
|
452
|
+
})(), msg.role === 'error' && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: " Error " }), _jsx(Text, { color: "red", wrap: "wrap", children: renderMarkdown(msg.content) })] })), msg.role === 'explain' && (_jsx(ExplainCard, { footer: "to switch: /explain off \u00B7 or set explainMode: false in ~/.glitool/config.json", children: renderMarkdown(msg.content) }))] }, i))), streamingContent !== '' && (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: 'cyan', children: "Assistant" }), _jsxs(Text, { wrap: "wrap", children: [streamingContent, "\u258A"] })] })), (statusState === 'working' || stageEvents.length > 0) && (stageEvents.length > 0
|
|
453
|
+
? _jsx(ProcessTrace, { events: stageEvents, active: statusState === 'working' })
|
|
454
|
+
: _jsx(ToolLog, { entries: toolLog })), _jsx(SlashPalette, { items: paletteItems, selectedIndex: paletteIndex }), confirmRequest ? (_jsx(ConfirmCard, { request: confirmRequest, onChoice: (choice) => {
|
|
455
|
+
if (choice === 'd')
|
|
456
|
+
return; // TODO: show full diff later
|
|
457
|
+
log('confirm:choice', { choice, hasResolver: !!confirmResolver });
|
|
458
|
+
const resolve = confirmResolver;
|
|
459
|
+
setConfirmRequest(null);
|
|
153
460
|
setConfirmResolver(null);
|
|
154
|
-
|
|
155
|
-
|
|
461
|
+
setStatusState('working');
|
|
462
|
+
resolve?.(choice === 'y');
|
|
463
|
+
log('confirm:resolved', { value: choice === 'y' });
|
|
464
|
+
} })) : showAuth ? (_jsx(AuthFlow, { onDone: (auth) => {
|
|
465
|
+
setShowAuth(false);
|
|
466
|
+
setMessages(prev => [...prev, {
|
|
467
|
+
role: 'assistant',
|
|
468
|
+
content: `✓ Signed in as ${auth.email} · ${auth.plan} · ${auth.requestsRemaining ?? 50} req/month`,
|
|
469
|
+
}]);
|
|
470
|
+
}, onCancel: () => setShowAuth(false) })) : (_jsxs(_Fragment, { children: [pastedContent && (() => {
|
|
471
|
+
const { headline, body } = pastePreview(pastedContent);
|
|
472
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.mustard, paddingX: 1, marginBottom: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.mustard, bold: true, children: "\uD83D\uDCCE PASTED " }), _jsx(Text, { color: colors.muted, children: headline }), _jsx(Text, { color: colors.muted, children: " \u00B7 " }), _jsx(Text, { color: colors.amber, bold: true, children: "Esc" }), _jsx(Text, { color: colors.muted, children: " to remove \u00B7 will prepend on send" })] }), _jsx(Text, { color: colors.muted, dimColor: true, children: body })] }));
|
|
473
|
+
})(), _jsxs(Box, { borderStyle: "round", borderColor: input.length > 200 ? 'yellow' : 'green', paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: " You: " }), _jsx(TextInput, { value: input, onChange: handleChange, onSubmit: handleSubmit, placeholder: "Type a message or /help..." }, inputKey), input.length > 50 && (_jsxs(Text, { dimColor: true, children: [" [", input.length, "]"] }))] })] })), escalation && (_jsx(EscalationCard, { payload: escalation, onChoice: (choice) => {
|
|
474
|
+
if (choice === 'approve') {
|
|
475
|
+
setEscalation(null);
|
|
476
|
+
}
|
|
477
|
+
else if (choice === 'abort') {
|
|
478
|
+
setMessages(prev => prev.slice(0, -1));
|
|
479
|
+
setEscalation(null);
|
|
480
|
+
setMessages(prev => [...prev, {
|
|
481
|
+
role: 'error',
|
|
482
|
+
content: 'Aborted. The last response was discarded.',
|
|
483
|
+
}]);
|
|
484
|
+
}
|
|
485
|
+
else if (choice === 'correct') {
|
|
486
|
+
setEscalation(null);
|
|
487
|
+
setMessages(prev => [...prev, {
|
|
488
|
+
role: 'assistant',
|
|
489
|
+
content: 'Tell me what to fix and I will retry.',
|
|
490
|
+
}]);
|
|
491
|
+
}
|
|
492
|
+
} }))] }), _jsx(StatusBar, { state: statusState, detail: statusDetail, tier: auth?.plan, anonLeft: anonLeft, model: config.preferredModel, tokens: tokens, cost: cost })] }));
|
|
156
493
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { colors } from './tokens.js';
|
|
5
|
+
import { startDeviceFlow, pollDeviceFlow, saveAuth } from '../auth.js';
|
|
6
|
+
const SPINNER = ['◐', '◓', '◑', '◒'];
|
|
7
|
+
export const AuthFlow = ({ onDone, onCancel }) => {
|
|
8
|
+
const [flow, setFlow] = useState(null);
|
|
9
|
+
const [phase, setPhase] = useState('loading');
|
|
10
|
+
const [errorMsg, setErrorMsg] = useState('');
|
|
11
|
+
const [frame, setFrame] = useState(0);
|
|
12
|
+
const pollTimer = useRef(null);
|
|
13
|
+
const spinTimer = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
spinTimer.current = setInterval(() => setFrame(f => (f + 1) % SPINNER.length), 180);
|
|
16
|
+
startDeviceFlow()
|
|
17
|
+
.then(data => {
|
|
18
|
+
setFlow(data);
|
|
19
|
+
setPhase('waiting');
|
|
20
|
+
pollTimer.current = setInterval(async () => {
|
|
21
|
+
try {
|
|
22
|
+
const result = await pollDeviceFlow(data.device_code);
|
|
23
|
+
if (result.status === 'complete' && result.access_token) {
|
|
24
|
+
clearInterval(pollTimer.current);
|
|
25
|
+
clearInterval(spinTimer.current);
|
|
26
|
+
const auth = {
|
|
27
|
+
token: result.access_token,
|
|
28
|
+
plan: result.plan ?? 'free',
|
|
29
|
+
email: result.email ?? '',
|
|
30
|
+
requestsRemaining: result.requests_remaining ?? 50,
|
|
31
|
+
resetDate: result.reset_date ?? '',
|
|
32
|
+
savedAt: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
saveAuth(auth);
|
|
35
|
+
setPhase('success');
|
|
36
|
+
setTimeout(() => onDone(auth), 1200);
|
|
37
|
+
}
|
|
38
|
+
else if (result.status === 'expired') {
|
|
39
|
+
clearInterval(pollTimer.current);
|
|
40
|
+
clearInterval(spinTimer.current);
|
|
41
|
+
setPhase('expired');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// network hiccup — keep polling
|
|
46
|
+
}
|
|
47
|
+
}, 5000);
|
|
48
|
+
})
|
|
49
|
+
.catch(err => {
|
|
50
|
+
clearInterval(spinTimer.current);
|
|
51
|
+
setPhase('error');
|
|
52
|
+
setErrorMsg(err?.message ?? 'Could not reach Glitool server');
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
if (pollTimer.current)
|
|
56
|
+
clearInterval(pollTimer.current);
|
|
57
|
+
if (spinTimer.current)
|
|
58
|
+
clearInterval(spinTimer.current);
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
if (phase === 'loading') {
|
|
62
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.amber, paddingX: 1, children: [_jsxs(Text, { color: colors.amber, children: [SPINNER[frame], " "] }), _jsx(Text, { color: colors.muted, children: "Connecting to Glitool..." })] }));
|
|
63
|
+
}
|
|
64
|
+
if (phase === 'error') {
|
|
65
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.rust, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.rust, children: ["\u2717 Sign-in failed: ", errorMsg] }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Type /signup to try again, or Esc to cancel." })] }));
|
|
66
|
+
}
|
|
67
|
+
if (phase === 'expired') {
|
|
68
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.rust, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.rust, children: "\u2717 Code expired." }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Type /signup to get a new code." })] }));
|
|
69
|
+
}
|
|
70
|
+
if (phase === 'success') {
|
|
71
|
+
return (_jsx(Box, { borderStyle: "round", borderColor: colors.sage, paddingX: 1, children: _jsx(Text, { color: colors.sage, children: "\u2713 Signed in! Glitool is ready." }) }));
|
|
72
|
+
}
|
|
73
|
+
// waiting
|
|
74
|
+
const activateUrl = `${flow.verification_uri}?code=${flow.user_code}`;
|
|
75
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.amber, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.amber, bold: true, children: "Sign in to Glitool (free \u00B7 50 req/month)" }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.muted, children: "1. Open this URL in your browser:" }), _jsxs(Text, { color: "cyan", children: [" ", activateUrl] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.muted, children: ["2. Your terminal code: ", _jsx(Text, { color: "white", bold: true, children: flow.user_code })] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.muted, children: [SPINNER[frame], " Waiting for sign-in... ", _jsx(Text, { dimColor: true, children: "(Esc to cancel)" })] })] }));
|
|
76
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { colors } from "./tokens.js";
|
|
5
|
+
import { symbols } from "./symbols.js";
|
|
6
|
+
function buildDiff(req) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
if (req.type === 'write') {
|
|
9
|
+
const all = (req.content ?? '').split('\n');
|
|
10
|
+
all.slice(0, 20).forEach(text => lines.push({ type: 'add', text }));
|
|
11
|
+
if (all.length > 20) {
|
|
12
|
+
lines.push({ type: 'context', text: `...${all.length - 20} more lines` });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
(req.oldString ?? '').split('\n').forEach(text => lines.push({ type: 'remove', text }));
|
|
17
|
+
(req.newString ?? '').split('\n').forEach(text => lines.push({ type: 'add', text }));
|
|
18
|
+
}
|
|
19
|
+
return lines;
|
|
20
|
+
}
|
|
21
|
+
const DiffLine = ({ line }) => {
|
|
22
|
+
if (line.type === 'add') {
|
|
23
|
+
return _jsxs(Text, { color: colors.sage, children: [" ", symbols.add, " ", line.text] });
|
|
24
|
+
}
|
|
25
|
+
if (line.type === 'remove') {
|
|
26
|
+
return _jsxs(Text, { color: colors.rust, children: [" ", symbols.remove, " ", line.text] });
|
|
27
|
+
}
|
|
28
|
+
return _jsxs(Text, { color: colors.muted, children: [" ", line.text] });
|
|
29
|
+
};
|
|
30
|
+
export const ConfirmCard = ({ request, onChoice }) => {
|
|
31
|
+
useInput((input, key) => {
|
|
32
|
+
const lower = input.toLowerCase();
|
|
33
|
+
if (lower === 'y' || key.return) {
|
|
34
|
+
onChoice('y');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (lower === 'n' || key.escape) {
|
|
38
|
+
onChoice('n');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (lower === 'd') {
|
|
42
|
+
onChoice('d');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const risk = request.risk ?? 'low';
|
|
47
|
+
const riskColor = risk === 'high' ? colors.rust : colors.mustard;
|
|
48
|
+
const verb = request.type === 'write' ? 'write' : 'edit';
|
|
49
|
+
const diffLines = buildDiff(request);
|
|
50
|
+
const added = diffLines.filter(l => l.type === 'add').length;
|
|
51
|
+
const removed = diffLines.filter(l => l.type === 'remove').length;
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: colors.mustard, bold: true, children: symbols.warning }), _jsxs(Text, { color: colors.ink, bold: true, children: ["glitool wants to ", verb, " ", request.filePath] }), _jsx(Text, { color: colors.muted, children: " " }), _jsxs(Text, { color: riskColor, bold: true, children: ["risk \u00B7 ", risk] })] }), _jsxs(Box, { borderStyle: "single", borderColor: colors.line, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: colors.muted, children: request.filePath }) }), _jsxs(Text, { color: colors.sage, children: ["+", added] }), _jsx(Text, { color: colors.muted, children: " " }), _jsxs(Text, { color: colors.rust, children: ["-", removed] })] }), diffLines.map((line, i) => (_jsx(DiffLine, { line: line }, i)))] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.muted, children: "Apply this change? " }), _jsx(Text, { color: colors.amber, bold: true, children: "[d]" }), _jsx(Text, { color: colors.muted, children: " view full diff " }), _jsx(Text, { color: colors.amber, bold: true, children: "[n]" }), _jsx(Text, { color: colors.muted, children: " reject " }), _jsx(Text, { color: colors.amber, bold: true, children: "[y]" }), _jsx(Text, { color: colors.muted, children: " approve" })] })] }));
|
|
53
|
+
};
|