@alia-codea/cli 1.1.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/dist/index.js +1263 -487
- package/package.json +5 -5
- package/src/app.tsx +281 -0
- package/src/commands/auth.ts +11 -1
- package/src/commands/repl.ts +11 -299
- package/src/commands/run.ts +103 -126
- package/src/commands/sessions.ts +5 -6
- package/src/components/ApprovalPrompt.tsx +60 -0
- package/src/components/Header.tsx +39 -0
- package/src/components/InputBar.tsx +36 -0
- package/src/components/MessageList.tsx +81 -0
- package/src/components/ThinkingIndicator.tsx +28 -0
- package/src/components/ToolCallCard.tsx +68 -0
- package/src/index.ts +20 -6
- package/src/tools/executor.ts +140 -14
- package/src/tools/patch.ts +167 -0
- package/src/utils/api.ts +22 -3
- package/src/utils/approval.ts +31 -0
- package/src/utils/context.ts +65 -4
- package/src/utils/conversation.ts +141 -0
- package/dist/api-X2G5QROW.js +0 -10
- package/dist/chunk-SVPL4GNV.js +0 -230
- package/dist/index.d.ts +0 -1
- package/src/utils/ui.ts +0 -153
package/src/commands/run.ts
CHANGED
|
@@ -1,33 +1,28 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
printToolExecution,
|
|
8
|
-
printToolResult,
|
|
9
|
-
showThinkingStatus,
|
|
10
|
-
hideThinkingStatus,
|
|
11
|
-
printAssistantPrefix,
|
|
12
|
-
printError,
|
|
13
|
-
printInfo
|
|
14
|
-
} from '../utils/ui.js';
|
|
15
|
-
|
|
16
|
-
interface Message {
|
|
17
|
-
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
18
|
-
content: string;
|
|
19
|
-
tool_calls?: any[];
|
|
20
|
-
tool_call_id?: string;
|
|
21
|
-
}
|
|
2
|
+
import { buildSystemMessage, getCodebaseContext, loadProjectInstructions } from '../utils/context.js';
|
|
3
|
+
import { processConversation, Message, ToolExecution } from '../utils/conversation.js';
|
|
4
|
+
import { ApprovalMode } from '../utils/approval.js';
|
|
5
|
+
import * as readline from 'readline';
|
|
22
6
|
|
|
23
7
|
interface RunOptions {
|
|
24
8
|
model: string;
|
|
25
9
|
yes: boolean;
|
|
26
10
|
context: boolean;
|
|
11
|
+
approvalMode?: string;
|
|
12
|
+
quiet?: boolean;
|
|
13
|
+
json?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface JsonOutput {
|
|
17
|
+
model: string;
|
|
18
|
+
prompt: string;
|
|
19
|
+
response: string;
|
|
20
|
+
tool_calls: Array<{ tool: string; args: Record<string, any>; result: string; success: boolean }>;
|
|
27
21
|
}
|
|
28
22
|
|
|
29
23
|
export async function runPrompt(prompt: string, options: RunOptions): Promise<void> {
|
|
30
24
|
const messages: Message[] = [];
|
|
25
|
+
const toolResults: JsonOutput['tool_calls'] = [];
|
|
31
26
|
|
|
32
27
|
// Get codebase context
|
|
33
28
|
let codebaseContext = '';
|
|
@@ -35,136 +30,118 @@ export async function runPrompt(prompt: string, options: RunOptions): Promise<vo
|
|
|
35
30
|
codebaseContext = await getCodebaseContext();
|
|
36
31
|
}
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
messages.push({ role: 'user', content: prompt });
|
|
40
|
-
|
|
41
|
-
// Build system message
|
|
42
|
-
const systemMessage = buildSystemMessage(options.model, codebaseContext);
|
|
43
|
-
|
|
44
|
-
// Process with tool loop
|
|
45
|
-
await processConversation(messages, systemMessage, options.model, options.yes);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function processConversation(
|
|
49
|
-
messages: Message[],
|
|
50
|
-
systemMessage: string,
|
|
51
|
-
model: string,
|
|
52
|
-
autoApprove: boolean
|
|
53
|
-
): Promise<void> {
|
|
54
|
-
let continueProcessing = true;
|
|
55
|
-
|
|
56
|
-
while (continueProcessing) {
|
|
57
|
-
printAssistantPrefix();
|
|
58
|
-
|
|
59
|
-
let fullContent = '';
|
|
60
|
-
let toolCalls: any[] | undefined;
|
|
61
|
-
|
|
62
|
-
showThinkingStatus('Thinking');
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
await streamChat(messages, systemMessage, model, {
|
|
66
|
-
onContent: (content) => {
|
|
67
|
-
hideThinkingStatus();
|
|
68
|
-
process.stdout.write(content);
|
|
69
|
-
fullContent += content;
|
|
70
|
-
},
|
|
71
|
-
onToolCall: () => {},
|
|
72
|
-
onDone: (content, tcs) => {
|
|
73
|
-
hideThinkingStatus();
|
|
74
|
-
toolCalls = tcs;
|
|
75
|
-
},
|
|
76
|
-
onError: (error) => {
|
|
77
|
-
hideThinkingStatus();
|
|
78
|
-
printError(error.message);
|
|
79
|
-
continueProcessing = false;
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
} catch (error: any) {
|
|
83
|
-
hideThinkingStatus();
|
|
84
|
-
printError(error.message);
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Handle tool calls
|
|
89
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
90
|
-
messages.push({
|
|
91
|
-
role: 'assistant',
|
|
92
|
-
content: fullContent,
|
|
93
|
-
tool_calls: toolCalls
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
if (fullContent) console.log();
|
|
33
|
+
const instructions = await loadProjectInstructions();
|
|
97
34
|
|
|
98
|
-
|
|
99
|
-
const args = JSON.parse(tc.function.arguments);
|
|
100
|
-
|
|
101
|
-
// Check if we need approval for file writes
|
|
102
|
-
const isDestructive = ['write_file', 'edit_file', 'run_command'].includes(tc.function.name);
|
|
103
|
-
|
|
104
|
-
if (isDestructive && !autoApprove) {
|
|
105
|
-
console.log();
|
|
106
|
-
console.log(chalk.yellow('⚠ ') + chalk.bold('Approval required:'));
|
|
107
|
-
console.log(formatToolCall(tc.function.name, args));
|
|
108
|
-
console.log();
|
|
35
|
+
messages.push({ role: 'user', content: prompt });
|
|
109
36
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
37
|
+
const systemMessage = buildSystemMessage(options.model, codebaseContext, instructions);
|
|
38
|
+
|
|
39
|
+
const approvalMode: ApprovalMode = options.yes
|
|
40
|
+
? 'full-auto'
|
|
41
|
+
: (options.approvalMode as ApprovalMode) || 'suggest';
|
|
42
|
+
|
|
43
|
+
let fullResponse = '';
|
|
44
|
+
|
|
45
|
+
await processConversation({
|
|
46
|
+
messages,
|
|
47
|
+
systemMessage,
|
|
48
|
+
model: options.model,
|
|
49
|
+
approvalMode,
|
|
50
|
+
isActive: () => true,
|
|
51
|
+
requestApproval: async (execution) => {
|
|
52
|
+
if (options.quiet || options.json) return false;
|
|
53
|
+
return askApproval(execution);
|
|
54
|
+
},
|
|
55
|
+
onEvent: (event) => {
|
|
56
|
+
switch (event.type) {
|
|
57
|
+
case 'thinking':
|
|
58
|
+
if (!options.quiet && !options.json) {
|
|
59
|
+
process.stdout.write(chalk.magenta('✦ '));
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case 'content':
|
|
63
|
+
fullResponse += event.text;
|
|
64
|
+
if (!options.quiet && !options.json) {
|
|
65
|
+
process.stdout.write(event.text);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case 'tool_start':
|
|
69
|
+
if (!options.quiet && !options.json) {
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(chalk.cyan(' → ') + chalk.bold(event.execution.tool) + ' ' + chalk.gray(formatArgs(event.execution)));
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
case 'tool_done':
|
|
75
|
+
if (event.execution.result !== undefined) {
|
|
76
|
+
toolResults.push({
|
|
77
|
+
tool: event.execution.tool,
|
|
78
|
+
args: event.execution.args,
|
|
79
|
+
result: event.execution.result,
|
|
80
|
+
success: event.execution.success ?? false,
|
|
116
81
|
});
|
|
117
|
-
continue;
|
|
118
82
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
83
|
+
if (!options.quiet && !options.json) {
|
|
84
|
+
const icon = event.execution.success ? chalk.green(' ✓') : chalk.red(' ✗');
|
|
85
|
+
const preview = (event.execution.result || '').slice(0, 100).replace(/\n/g, ' ');
|
|
86
|
+
console.log(`${icon} ${chalk.gray(preview)}`);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case 'done':
|
|
90
|
+
if (!options.quiet && !options.json) {
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'error':
|
|
95
|
+
if (!options.json) {
|
|
96
|
+
console.error(chalk.red('Error: ') + event.message);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
134
99
|
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
135
102
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
103
|
+
if (options.json) {
|
|
104
|
+
const output: JsonOutput = {
|
|
105
|
+
model: options.model,
|
|
106
|
+
prompt,
|
|
107
|
+
response: fullResponse,
|
|
108
|
+
tool_calls: toolResults,
|
|
109
|
+
};
|
|
110
|
+
console.log(JSON.stringify(output, null, 2));
|
|
111
|
+
} else if (options.quiet) {
|
|
112
|
+
if (fullResponse) {
|
|
113
|
+
console.log(fullResponse);
|
|
143
114
|
}
|
|
144
115
|
}
|
|
145
116
|
}
|
|
146
117
|
|
|
147
|
-
async function askApproval(): Promise<boolean> {
|
|
148
|
-
const readline = await import('readline');
|
|
118
|
+
async function askApproval(execution: ToolExecution): Promise<boolean> {
|
|
149
119
|
const rl = readline.createInterface({
|
|
150
120
|
input: process.stdin,
|
|
151
|
-
output: process.stdout
|
|
121
|
+
output: process.stdout,
|
|
152
122
|
});
|
|
153
123
|
|
|
124
|
+
const desc = formatArgs(execution);
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(chalk.yellow('⚠ ') + chalk.bold(execution.tool) + ' ' + desc);
|
|
127
|
+
|
|
154
128
|
return new Promise((resolve) => {
|
|
155
|
-
rl.question(chalk.cyan('Allow? [y/N] '), (answer) => {
|
|
129
|
+
rl.question(chalk.cyan(' Allow? [y/N] '), (answer) => {
|
|
156
130
|
rl.close();
|
|
157
131
|
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
158
132
|
});
|
|
159
133
|
});
|
|
160
134
|
}
|
|
161
135
|
|
|
162
|
-
function
|
|
163
|
-
|
|
136
|
+
function formatArgs(execution: ToolExecution): string {
|
|
137
|
+
const { tool, args } = execution;
|
|
138
|
+
switch (tool) {
|
|
164
139
|
case 'read_file':
|
|
165
140
|
case 'write_file':
|
|
166
141
|
case 'edit_file':
|
|
167
142
|
return args.path || '';
|
|
143
|
+
case 'apply_patch':
|
|
144
|
+
return 'applying patch...';
|
|
168
145
|
case 'list_files':
|
|
169
146
|
return args.path || '.';
|
|
170
147
|
case 'search_files':
|
|
@@ -172,6 +149,6 @@ function formatToolArgs(name: string, args: Record<string, any>): string {
|
|
|
172
149
|
case 'run_command':
|
|
173
150
|
return args.command || '';
|
|
174
151
|
default:
|
|
175
|
-
return JSON.stringify(args).slice(0,
|
|
152
|
+
return JSON.stringify(args).slice(0, 60);
|
|
176
153
|
}
|
|
177
154
|
}
|
package/src/commands/sessions.ts
CHANGED
|
@@ -2,13 +2,12 @@ import chalk from 'chalk';
|
|
|
2
2
|
import * as readline from 'readline';
|
|
3
3
|
import { getSessions, getSession, config } from '../utils/config.js';
|
|
4
4
|
import { startRepl } from './repl.js';
|
|
5
|
-
import { printBanner, printError, printInfo } from '../utils/ui.js';
|
|
6
5
|
|
|
7
6
|
export async function listSessions(): Promise<void> {
|
|
8
7
|
const sessions = getSessions();
|
|
9
8
|
|
|
10
9
|
if (sessions.length === 0) {
|
|
11
|
-
|
|
10
|
+
console.log(chalk.blue('ℹ ') + 'No saved sessions found.');
|
|
12
11
|
console.log(chalk.gray('Start a new session with: ') + chalk.cyan('codea'));
|
|
13
12
|
return;
|
|
14
13
|
}
|
|
@@ -38,7 +37,7 @@ export async function resumeSession(sessionId?: string): Promise<void> {
|
|
|
38
37
|
const sessions = getSessions();
|
|
39
38
|
|
|
40
39
|
if (sessions.length === 0) {
|
|
41
|
-
|
|
40
|
+
console.log(chalk.blue('ℹ ') + 'No saved sessions found.');
|
|
42
41
|
return;
|
|
43
42
|
}
|
|
44
43
|
|
|
@@ -54,7 +53,7 @@ export async function resumeSession(sessionId?: string): Promise<void> {
|
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
if (!selectedSession) {
|
|
57
|
-
|
|
56
|
+
console.log(chalk.red('✗ Error: ') + `Session not found: ${sessionId}`);
|
|
58
57
|
return;
|
|
59
58
|
}
|
|
60
59
|
} else {
|
|
@@ -82,7 +81,7 @@ export async function resumeSession(sessionId?: string): Promise<void> {
|
|
|
82
81
|
|
|
83
82
|
const index = parseInt(answer) - 1;
|
|
84
83
|
if (isNaN(index) || index < 0 || index >= sessions.length) {
|
|
85
|
-
|
|
84
|
+
console.log(chalk.red('✗ Error: ') + 'Invalid selection.');
|
|
86
85
|
resolve();
|
|
87
86
|
return;
|
|
88
87
|
}
|
|
@@ -99,7 +98,7 @@ export async function resumeSession(sessionId?: string): Promise<void> {
|
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
async function startRestoredSession(session: any): Promise<void> {
|
|
102
|
-
|
|
101
|
+
console.log(chalk.blue('ℹ ') + `Resuming: ${session.title}`);
|
|
103
102
|
console.log();
|
|
104
103
|
|
|
105
104
|
// Display previous messages
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { ToolExecution } from '../utils/conversation.js';
|
|
4
|
+
|
|
5
|
+
interface ApprovalPromptProps {
|
|
6
|
+
execution: ToolExecution;
|
|
7
|
+
onResolve: (approved: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatArgs(tool: string, args: Record<string, any>): string {
|
|
11
|
+
switch (tool) {
|
|
12
|
+
case 'write_file':
|
|
13
|
+
return `Write to ${args.path}`;
|
|
14
|
+
case 'edit_file':
|
|
15
|
+
return `Edit ${args.path}`;
|
|
16
|
+
case 'apply_patch':
|
|
17
|
+
return `Apply patch`;
|
|
18
|
+
case 'run_command':
|
|
19
|
+
return `Run: ${args.command}`;
|
|
20
|
+
default:
|
|
21
|
+
return `${tool}: ${JSON.stringify(args).slice(0, 80)}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ApprovalPrompt({ execution, onResolve }: ApprovalPromptProps) {
|
|
26
|
+
useInput((input, key) => {
|
|
27
|
+
if (input === 'y' || input === 'Y') {
|
|
28
|
+
onResolve(true);
|
|
29
|
+
} else if (input === 'n' || input === 'N' || key.escape) {
|
|
30
|
+
onResolve(false);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const description = formatArgs(execution.tool, execution.args);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Box flexDirection="column" paddingLeft={2} paddingY={0}>
|
|
38
|
+
<Box gap={1}>
|
|
39
|
+
<Text color="yellow">{'⚠'}</Text>
|
|
40
|
+
<Text bold>{description}</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
{execution.tool === 'run_command' && execution.args.command && (
|
|
43
|
+
<Box paddingLeft={2}>
|
|
44
|
+
<Text color="gray">$ {execution.args.command}</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
)}
|
|
47
|
+
{execution.tool === 'write_file' && execution.args.content && (
|
|
48
|
+
<Box paddingLeft={2}>
|
|
49
|
+
<Text color="gray">{execution.args.content.split('\n').length} lines</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
)}
|
|
52
|
+
<Box paddingLeft={2} gap={1}>
|
|
53
|
+
<Text color="green">[y]</Text>
|
|
54
|
+
<Text>approve</Text>
|
|
55
|
+
<Text color="red">[n]</Text>
|
|
56
|
+
<Text>deny</Text>
|
|
57
|
+
</Box>
|
|
58
|
+
</Box>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { ApprovalMode } from '../utils/approval.js';
|
|
4
|
+
|
|
5
|
+
interface HeaderProps {
|
|
6
|
+
cwd: string;
|
|
7
|
+
model: string;
|
|
8
|
+
approvalMode: ApprovalMode;
|
|
9
|
+
contextPercent: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shortenPath(p: string): string {
|
|
13
|
+
const home = process.env.HOME || '';
|
|
14
|
+
if (home && p.startsWith(home)) {
|
|
15
|
+
return '~' + p.slice(home.length);
|
|
16
|
+
}
|
|
17
|
+
return p;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MODE_COLORS: Record<ApprovalMode, string> = {
|
|
21
|
+
'suggest': 'yellow',
|
|
22
|
+
'auto-edit': 'cyan',
|
|
23
|
+
'full-auto': 'green',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function Header({ cwd, model, approvalMode, contextPercent }: HeaderProps) {
|
|
27
|
+
return (
|
|
28
|
+
<Box borderStyle="single" borderColor="gray" paddingX={1} justifyContent="space-between">
|
|
29
|
+
<Text color="cyan">{shortenPath(cwd)}</Text>
|
|
30
|
+
<Box gap={1}>
|
|
31
|
+
<Text color="magenta">{model}</Text>
|
|
32
|
+
<Text color="gray">|</Text>
|
|
33
|
+
<Text color={MODE_COLORS[approvalMode]}>{approvalMode}</Text>
|
|
34
|
+
<Text color="gray">|</Text>
|
|
35
|
+
<Text color={contextPercent < 20 ? 'red' : 'gray'}>{contextPercent}% left</Text>
|
|
36
|
+
</Box>
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { ThinkingIndicator } from './ThinkingIndicator.js';
|
|
5
|
+
|
|
6
|
+
interface InputBarProps {
|
|
7
|
+
onSubmit: (value: string) => void;
|
|
8
|
+
isProcessing: boolean;
|
|
9
|
+
thinkingLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function InputBar({ onSubmit, isProcessing, thinkingLabel }: InputBarProps) {
|
|
13
|
+
const [value, setValue] = useState('');
|
|
14
|
+
|
|
15
|
+
const handleSubmit = (text: string) => {
|
|
16
|
+
const trimmed = text.trim();
|
|
17
|
+
if (!trimmed) return;
|
|
18
|
+
setValue('');
|
|
19
|
+
onSubmit(trimmed);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (isProcessing) {
|
|
23
|
+
return (
|
|
24
|
+
<Box paddingX={1}>
|
|
25
|
+
<ThinkingIndicator label={thinkingLabel || 'Thinking'} />
|
|
26
|
+
</Box>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Box paddingX={1}>
|
|
32
|
+
<Text color="cyan">{'❯ '}</Text>
|
|
33
|
+
<TextInput value={value} onChange={setValue} onSubmit={handleSubmit} />
|
|
34
|
+
</Box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Marked } from 'marked';
|
|
4
|
+
import { markedTerminal } from 'marked-terminal';
|
|
5
|
+
import { ToolCallCard } from './ToolCallCard.js';
|
|
6
|
+
import { ToolExecution } from '../utils/conversation.js';
|
|
7
|
+
|
|
8
|
+
export interface DisplayMessage {
|
|
9
|
+
id: string;
|
|
10
|
+
type: 'user' | 'assistant' | 'tool' | 'info';
|
|
11
|
+
content: string;
|
|
12
|
+
toolExecution?: ToolExecution;
|
|
13
|
+
streaming?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface MessageListProps {
|
|
17
|
+
messages: DisplayMessage[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const marked = new Marked(markedTerminal() as any);
|
|
21
|
+
|
|
22
|
+
function renderMarkdown(text: string): string {
|
|
23
|
+
try {
|
|
24
|
+
const rendered = marked.parse(text);
|
|
25
|
+
if (typeof rendered === 'string') {
|
|
26
|
+
return rendered.trimEnd();
|
|
27
|
+
}
|
|
28
|
+
return text;
|
|
29
|
+
} catch {
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function MessageBlock({ message }: { message: DisplayMessage }) {
|
|
35
|
+
switch (message.type) {
|
|
36
|
+
case 'user':
|
|
37
|
+
return (
|
|
38
|
+
<Box paddingLeft={1} paddingY={0}>
|
|
39
|
+
<Text color="cyan">{'❯ '}</Text>
|
|
40
|
+
<Text>{message.content}</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
case 'assistant':
|
|
45
|
+
return (
|
|
46
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
47
|
+
<Box>
|
|
48
|
+
<Text color="magenta">{'✦ '}</Text>
|
|
49
|
+
<Text>{message.streaming ? message.content : renderMarkdown(message.content)}</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
</Box>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
case 'tool':
|
|
55
|
+
if (message.toolExecution) {
|
|
56
|
+
return <ToolCallCard execution={message.toolExecution} />;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
|
|
60
|
+
case 'info':
|
|
61
|
+
return (
|
|
62
|
+
<Box paddingLeft={1}>
|
|
63
|
+
<Text color="blue">{'ℹ '}</Text>
|
|
64
|
+
<Text color="gray">{message.content}</Text>
|
|
65
|
+
</Box>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
default:
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function MessageList({ messages }: MessageListProps) {
|
|
74
|
+
return (
|
|
75
|
+
<Box flexDirection="column" flexGrow={1} gap={0}>
|
|
76
|
+
{messages.map((msg) => (
|
|
77
|
+
<MessageBlock key={msg.id} message={msg} />
|
|
78
|
+
))}
|
|
79
|
+
</Box>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
|
|
5
|
+
interface ThinkingIndicatorProps {
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ThinkingIndicator({ label = 'Thinking' }: ThinkingIndicatorProps) {
|
|
10
|
+
const [elapsed, setElapsed] = useState(0);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const interval = setInterval(() => {
|
|
14
|
+
setElapsed((e) => e + 1);
|
|
15
|
+
}, 1000);
|
|
16
|
+
return () => clearInterval(interval);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box gap={1}>
|
|
21
|
+
<Text color="cyan">
|
|
22
|
+
<Spinner type="dots" />
|
|
23
|
+
</Text>
|
|
24
|
+
<Text bold>{label}</Text>
|
|
25
|
+
<Text color="gray">({elapsed}s)</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { ToolExecution } from '../utils/conversation.js';
|
|
4
|
+
|
|
5
|
+
interface ToolCallCardProps {
|
|
6
|
+
execution: ToolExecution;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatArgs(tool: string, args: Record<string, any>): string {
|
|
10
|
+
switch (tool) {
|
|
11
|
+
case 'read_file':
|
|
12
|
+
case 'write_file':
|
|
13
|
+
case 'edit_file':
|
|
14
|
+
return args.path || '';
|
|
15
|
+
case 'apply_patch':
|
|
16
|
+
return 'applying patch...';
|
|
17
|
+
case 'list_files':
|
|
18
|
+
return args.path || '.';
|
|
19
|
+
case 'search_files':
|
|
20
|
+
return `"${args.pattern}" in ${args.path || '.'}`;
|
|
21
|
+
case 'run_command':
|
|
22
|
+
return args.command || '';
|
|
23
|
+
default:
|
|
24
|
+
return JSON.stringify(args).slice(0, 60);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ToolCallCard({ execution }: ToolCallCardProps) {
|
|
29
|
+
const { tool, args, result, success, approved } = execution;
|
|
30
|
+
const argStr = formatArgs(tool, args);
|
|
31
|
+
const isDone = result !== undefined;
|
|
32
|
+
|
|
33
|
+
if (approved === false) {
|
|
34
|
+
return (
|
|
35
|
+
<Box flexDirection="column" paddingLeft={2}>
|
|
36
|
+
<Box gap={1}>
|
|
37
|
+
<Text color="red">{'✗'}</Text>
|
|
38
|
+
<Text bold color="gray" strikethrough>{tool}</Text>
|
|
39
|
+
<Text color="gray">{argStr}</Text>
|
|
40
|
+
</Box>
|
|
41
|
+
<Box paddingLeft={2}>
|
|
42
|
+
<Text color="yellow" dimColor>Declined by user</Text>
|
|
43
|
+
</Box>
|
|
44
|
+
</Box>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Box flexDirection="column" paddingLeft={2}>
|
|
50
|
+
<Box gap={1}>
|
|
51
|
+
{isDone ? (
|
|
52
|
+
<Text color={success ? 'green' : 'red'}>{success ? '✓' : '✗'}</Text>
|
|
53
|
+
) : (
|
|
54
|
+
<Text color="cyan">{'→'}</Text>
|
|
55
|
+
)}
|
|
56
|
+
<Text bold>{tool}</Text>
|
|
57
|
+
<Text color="gray">{argStr}</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
{isDone && result && (
|
|
60
|
+
<Box paddingLeft={2}>
|
|
61
|
+
<Text color="gray" wrap="truncate-end">
|
|
62
|
+
{result.slice(0, 120).replace(/\n/g, ' ')}{result.length > 120 ? '...' : ''}
|
|
63
|
+
</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
)}
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|