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.
Files changed (56) hide show
  1. package/README.md +115 -45
  2. package/dist/agent.js +234 -37
  3. package/dist/agents/coder.js +46 -34
  4. package/dist/agents/debugger.js +111 -0
  5. package/dist/agents/explainer.js +2 -5
  6. package/dist/agents/git-agent.js +90 -0
  7. package/dist/agents/graph.js +214 -23
  8. package/dist/agents/judge.js +61 -0
  9. package/dist/agents/planner.js +31 -10
  10. package/dist/agents/planningAgent.js +41 -0
  11. package/dist/agents/refactorer.js +97 -0
  12. package/dist/agents/reviewer-agent.js +87 -0
  13. package/dist/agents/reviewer.js +6 -9
  14. package/dist/agents/types.js +1 -0
  15. package/dist/agents/validator.js +93 -0
  16. package/dist/agents/workflow.js +45 -0
  17. package/dist/auth.js +87 -0
  18. package/dist/commands/version.js +1 -0
  19. package/dist/config.js +4 -1
  20. package/dist/confirmHandler.js +4 -2
  21. package/dist/index.js +12 -25
  22. package/dist/llm/classifier.js +61 -0
  23. package/dist/llm/factory.js +50 -0
  24. package/dist/llm/router.js +235 -14
  25. package/dist/llm/telemetry.js +18 -0
  26. package/dist/logger.js +25 -0
  27. package/dist/processEvents.js +1 -0
  28. package/dist/tools/bashTool.js +90 -0
  29. package/dist/tools/editFileTool.js +14 -3
  30. package/dist/tools/index.js +3 -1
  31. package/dist/tools/listFilesTool.js +19 -21
  32. package/dist/tools/processRegistry.js +36 -0
  33. package/dist/tools/readBackgroundOutput.js +29 -0
  34. package/dist/tools/readFileTool.js +64 -9
  35. package/dist/tools/searchCodeTool.js +14 -4
  36. package/dist/tools/webFetchTool.js +45 -0
  37. package/dist/tools/writeFileTool.js +9 -6
  38. package/dist/trust/riskScorer.js +29 -2
  39. package/dist/ui/App.js +384 -47
  40. package/dist/ui/AuthFlow.js +76 -0
  41. package/dist/ui/ConfirmCard.js +53 -0
  42. package/dist/ui/EscalationCard.js +22 -0
  43. package/dist/ui/ExplainCard.js +5 -0
  44. package/dist/ui/Pipeline.js +37 -0
  45. package/dist/ui/ProcessTrace.js +79 -0
  46. package/dist/ui/RoleRow.js +16 -0
  47. package/dist/ui/RoleRow.test.js +8 -0
  48. package/dist/ui/SlashPalette.js +32 -0
  49. package/dist/ui/StatusBar.js +44 -0
  50. package/dist/ui/ToolLog.js +62 -0
  51. package/dist/ui/Welcome.js +11 -0
  52. package/dist/ui/renderMarkdown.js +41 -0
  53. package/dist/ui/symbols.js +19 -0
  54. package/dist/ui/tokens.js +13 -0
  55. package/dist/version.js +1 -0
  56. 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, useCallback } from "react";
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
- const COMMANDS = ['/help', '/clear', 'model', '/tools', '/exit', '/reset'];
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 [isThinking, setIsThinking] = useState(false);
17
- const [toolInfo, setToolInfo] = useState('');
18
- const [suggestions, setSuggestions] = useState([]);
19
- const [confirmMessage, setConfirmMessage] = useState('');
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
- setInput(value);
27
- if (value.startsWith('/')) {
28
- const matches = COMMANDS.filter(c => c.startsWith(value));
29
- setSuggestions(value === matches[0] ? [] : matches);
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
- setSuggestions([]);
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((_, key) => {
41
- if (confirmMessage)
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
- if (isThinking)
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((messages) => {
215
+ setConfirmHandler((req) => {
69
216
  return new Promise((resolve) => {
70
- setConfirmMessage(messages);
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 = useCallback(async (value) => {
76
- const cmd = value.trim();
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: 'Commands: /exit /clear /model /tools /help'
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: 'Model: gpr-5-mini' }]);
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: 'Tools: listFile readFile searchCode editFile writeFile analyzeProject'
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
- setIsThinking(true);
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
- const argStr = args ? String(Object.values(args)[0]) : '';
122
- setToolInfo(`⚙ ${toolName}${argStr ? ` -> ${argStr}` : ''} `);
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
- setToolInfo(status);
125
- }, (token) => {
126
- setStreamingContent(prev => prev + token);
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
- setIsThinking(false);
145
- setToolInfo('');
146
- }
147
- }, [exit]);
148
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "green", children: "glitool" }), _jsx(Text, { dimColor: true, children: "- AI coding assistant" })] }), 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 === 'assistant' && (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: " Assistant " }), _jsxs(Text, { color: "red", wrap: "wrap", children: [" ", msg.content, " "] })] })), 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: msg.content })] })), msg.role === 'explain' && (_jsxs(Box, { borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: "\uD83D\uDCA1 Explain " }), _jsx(Text, { wrap: "wrap", children: 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"] })] })), isThinking && (_jsx(Box, { marginBottom: 1, padding: 1, children: _jsx(Text, { color: "yellow", children: toolInfo || 'Thinking...' }) })), suggestions.length > 0 && (_jsx(Box, { flexDirection: "column", marginBottom: 1, paddingX: 2, children: suggestions.map(s => (_jsx(Text, { dimColor: true, children: s }, s))) })), confirmMessage !== '' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Text, { color: "yellow", children: [" ", confirmMessage, " (y/n): "] }), _jsx(TextInput, { value: confirmInput, onChange: setConfirmInput, onSubmit: (val) => {
149
- const approved = val.toLowerCase() === 'y' || val === '';
150
- confirmResolver?.(approved);
151
- setConfirmMessage('');
152
- setConfirmInput('');
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
- _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..." }), input.length > 50 && (_jsxs(Text, { dimColor: true, children: [" [", input.length, "]"] }))] })] }));
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
+ };