halfcopilot 0.0.1

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 (50) hide show
  1. package/dist/halfcop.d.ts +6 -0
  2. package/dist/halfcop.d.ts.map +1 -0
  3. package/dist/halfcop.js +1103 -0
  4. package/dist/halfcop.js.map +1 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +255 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/tui/app.d.ts +11 -0
  10. package/dist/tui/app.d.ts.map +1 -0
  11. package/dist/tui/app.js +105 -0
  12. package/dist/tui/app.js.map +1 -0
  13. package/dist/tui/components/ChatView.d.ts +12 -0
  14. package/dist/tui/components/ChatView.d.ts.map +1 -0
  15. package/dist/tui/components/ChatView.js +70 -0
  16. package/dist/tui/components/ChatView.js.map +1 -0
  17. package/dist/tui/components/InputField.d.ts +8 -0
  18. package/dist/tui/components/InputField.d.ts.map +1 -0
  19. package/dist/tui/components/InputField.js +24 -0
  20. package/dist/tui/components/InputField.js.map +1 -0
  21. package/dist/tui/components/StatusBar.d.ts +18 -0
  22. package/dist/tui/components/StatusBar.d.ts.map +1 -0
  23. package/dist/tui/components/StatusBar.js +57 -0
  24. package/dist/tui/components/StatusBar.js.map +1 -0
  25. package/dist/tui/components/ToolApproval.d.ts +11 -0
  26. package/dist/tui/components/ToolApproval.d.ts.map +1 -0
  27. package/dist/tui/components/ToolApproval.js +41 -0
  28. package/dist/tui/components/ToolApproval.js.map +1 -0
  29. package/dist/tui/components/index.d.ts +5 -0
  30. package/dist/tui/components/index.d.ts.map +1 -0
  31. package/dist/tui/components/index.js +5 -0
  32. package/dist/tui/components/index.js.map +1 -0
  33. package/dist/tui/index.d.ts +3 -0
  34. package/dist/tui/index.d.ts.map +1 -0
  35. package/dist/tui/index.js +3 -0
  36. package/dist/tui/index.js.map +1 -0
  37. package/package.json +43 -0
  38. package/src/__tests__/cli.test.ts +73 -0
  39. package/src/halfcop.ts +1373 -0
  40. package/src/index.ts +348 -0
  41. package/src/tui/app.tsx +160 -0
  42. package/src/tui/components/ChatView.tsx +130 -0
  43. package/src/tui/components/InputField.tsx +41 -0
  44. package/src/tui/components/StatusBar.tsx +92 -0
  45. package/src/tui/components/ToolApproval.tsx +80 -0
  46. package/src/tui/components/index.ts +4 -0
  47. package/src/tui/index.ts +7 -0
  48. package/tsconfig.json +20 -0
  49. package/tsconfig.tsbuildinfo +1 -0
  50. package/vitest.config.ts +7 -0
package/src/index.ts ADDED
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { loadConfig } from "@halfcopilot/config";
5
+ import { ProviderRegistry } from "@halfcopilot/provider";
6
+ import {
7
+ ToolRegistry,
8
+ createBuiltinTools,
9
+ PermissionChecker,
10
+ ToolExecutor,
11
+ } from "@halfcopilot/tools";
12
+ import { AgentLoop, HybridProvider, type AgentMode } from "@halfcopilot/core";
13
+ import { MemoryStore } from "@halfcopilot/memory";
14
+ import { SkillRegistry, createBuiltinSkills } from "@halfcopilot/skills";
15
+ import { MCPClientManager } from "@halfcopilot/mcp";
16
+
17
+ const program = new Command();
18
+
19
+ program
20
+ .name("halfcopilot")
21
+ .description("HalfCopilot — Multi-model Agent Framework CLI with Hybrid Mode")
22
+ .version("0.0.1");
23
+
24
+ function createAgent(options: {
25
+ model?: string;
26
+ provider?: string;
27
+ mode?: string;
28
+ hybrid?: boolean;
29
+ }) {
30
+ const config = loadConfig();
31
+ const providerRegistry = new ProviderRegistry();
32
+ providerRegistry.createFromConfig(config);
33
+
34
+ const providerName = options.provider ?? config.defaultProvider ?? "xiaomi";
35
+ let provider = providerRegistry.get(providerName);
36
+
37
+ // Apply hybrid mode if requested
38
+ if (options.hybrid) {
39
+ provider = new HybridProvider(provider);
40
+ }
41
+
42
+ const toolRegistry = new ToolRegistry();
43
+ const builtinTools = createBuiltinTools();
44
+ builtinTools.forEach((t) => toolRegistry.register(t));
45
+
46
+ // Load skills
47
+ const skillRegistry = new SkillRegistry();
48
+ const builtinSkills = createBuiltinSkills();
49
+ builtinSkills.forEach((s) => skillRegistry.register(s));
50
+
51
+ const permissions = new PermissionChecker({
52
+ autoApproveSafe: config.permissions.autoApproveSafe,
53
+ allow: config.permissions.allow,
54
+ deny: config.permissions.deny,
55
+ });
56
+
57
+ const executor = new ToolExecutor(toolRegistry, permissions);
58
+
59
+ const agent = new AgentLoop({
60
+ provider,
61
+ model: options.model ?? config.defaultModel ?? "mimo-v2.5-pro",
62
+ tools: toolRegistry,
63
+ executor,
64
+ permissions,
65
+ maxTurns: config.maxTurns,
66
+ mode: (options.mode as AgentMode) ?? "auto",
67
+ });
68
+
69
+ return { agent, providerName, config, skillRegistry };
70
+ }
71
+
72
+ program
73
+ .command("chat")
74
+ .description("Start interactive chat mode")
75
+ .option("-m, --model <model>", "Model to use")
76
+ .option("-p, --provider <provider>", "Provider to use")
77
+ .option("--mode <mode>", "Agent mode (plan/review/act/auto)", "auto")
78
+ .option(
79
+ "--hybrid",
80
+ "Enable hybrid mode (text block parsing for non-tool-use models)",
81
+ )
82
+ .option("--tui", "Enable TUI mode (requires ink/React)")
83
+ .action(async (options) => {
84
+ try {
85
+ const { agent, providerName, config, skillRegistry } =
86
+ createAgent(options);
87
+
88
+ // Try to use TUI if requested
89
+ if (options.tui) {
90
+ try {
91
+ const { render } = await import("ink");
92
+ const React = await import("react");
93
+ const { App } = await import("./tui/app.js");
94
+
95
+ const { waitUntilExit } = render(
96
+ React.createElement(App, {
97
+ agent,
98
+ providerName,
99
+ model: options.model ?? config.defaultModel ?? "mimo-v2.5-pro",
100
+ mode: options.mode ?? "auto",
101
+ }),
102
+ );
103
+
104
+ await waitUntilExit();
105
+ return;
106
+ } catch {
107
+ // Fallback to simple CLI
108
+ console.log("TUI mode not available, falling back to simple mode\n");
109
+ }
110
+ }
111
+
112
+ // Simple CLI mode
113
+ console.log(
114
+ "\x1b[36m╔══════════════════════════════════════════════════╗\x1b[0m",
115
+ );
116
+ console.log(
117
+ "\x1b[36m║ HalfCopilot v0.0.1 ║\x1b[0m",
118
+ );
119
+ console.log(
120
+ "\x1b[36m║ Multi-model Agent Framework CLI ║\x1b[0m",
121
+ );
122
+ console.log(
123
+ "\x1b[36m╚══════════════════════════════════════════════════╝\x1b[0m",
124
+ );
125
+ console.log(`\x1b[32mProvider:\x1b[0m ${providerName}`);
126
+ console.log(
127
+ `\x1b[32mModel:\x1b[0m ${options.model ?? config.defaultModel ?? "mimo-v2.5-pro"}`,
128
+ );
129
+ console.log(`\x1b[32mMode:\x1b[0m ${options.mode ?? "auto"}`);
130
+ if (options.hybrid) {
131
+ console.log(`\x1b[33mHybrid Mode: Enabled\x1b[0m`);
132
+ }
133
+ console.log(
134
+ `\x1b[33mType your message and press Enter. Type 'exit' to quit.\x1b[0m\n`,
135
+ );
136
+
137
+ const readline = await import("readline");
138
+ const rl = readline.createInterface({
139
+ input: process.stdin,
140
+ output: process.stdout,
141
+ });
142
+
143
+ const ask = () => {
144
+ rl.question("\x1b[32m❯ \x1b[0m", async (input) => {
145
+ if (
146
+ input.trim().toLowerCase() === "exit" ||
147
+ input.trim().toLowerCase() === "quit"
148
+ ) {
149
+ console.log("\x1b[33mGoodbye!\x1b[0m");
150
+ rl.close();
151
+ return;
152
+ }
153
+
154
+ if (input.trim() === "") {
155
+ ask();
156
+ return;
157
+ }
158
+
159
+ // Check for skill triggers
160
+ const matchedSkills = skillRegistry.findByTrigger(input);
161
+ if (matchedSkills.length > 0) {
162
+ console.log(
163
+ `\x1b[35m[Skills matched: ${matchedSkills.map((s) => s.name).join(", ")}]\x1b[0m`,
164
+ );
165
+ }
166
+
167
+ try {
168
+ for await (const event of agent.run(input)) {
169
+ if (event.type === "text") {
170
+ process.stdout.write(event.content ?? "");
171
+ } else if (event.type === "tool_use") {
172
+ console.log(`\n\x1b[35m[Tool: ${event.toolName}]\x1b[0m`);
173
+ } else if (event.type === "tool_result") {
174
+ const result = event.toolOutput ?? "";
175
+ const truncated =
176
+ result.length > 200 ? result.slice(0, 200) + "..." : result;
177
+ console.log(`\x1b[36m[Result: ${truncated}]\x1b[0m`);
178
+ } else if (event.type === "error") {
179
+ console.log(`\x1b[31m[Error: ${event.error?.message}]\x1b[0m`);
180
+ } else if (event.type === "done") {
181
+ console.log("\n");
182
+ }
183
+ }
184
+ } catch (err) {
185
+ console.log(
186
+ `\x1b[31m[Error: ${err instanceof Error ? err.message : err}]\x1b[0m`,
187
+ );
188
+ }
189
+
190
+ ask();
191
+ });
192
+ };
193
+
194
+ ask();
195
+ } catch (err) {
196
+ console.error(
197
+ "\x1b[31mError:\x1b[0m",
198
+ err instanceof Error ? err.message : err,
199
+ );
200
+ process.exit(1);
201
+ }
202
+ });
203
+
204
+ program
205
+ .command("run <prompt>")
206
+ .description("Run a single prompt and exit")
207
+ .option("-m, --model <model>", "Model to use")
208
+ .option("-p, --provider <provider>", "Provider to use")
209
+ .option("--mode <mode>", "Agent mode (plan/review/act/auto)", "act")
210
+ .option("--hybrid", "Enable hybrid mode")
211
+ .action(async (prompt, options) => {
212
+ try {
213
+ const { agent } = createAgent(options);
214
+
215
+ for await (const event of agent.run(prompt)) {
216
+ if (event.type === "text") {
217
+ process.stdout.write(event.content ?? "");
218
+ } else if (event.type === "tool_use") {
219
+ console.log(`\n\x1b[35m[Tool: ${event.toolName}]\x1b[0m`);
220
+ } else if (event.type === "tool_result") {
221
+ const result = event.toolOutput ?? "";
222
+ const truncated =
223
+ result.length > 200 ? result.slice(0, 200) + "..." : result;
224
+ console.log(`\x1b[36m[Result: ${truncated}]\x1b[0m`);
225
+ }
226
+ }
227
+
228
+ console.log("\n");
229
+ } catch (err) {
230
+ console.error(
231
+ "\x1b[31mError:\x1b[0m",
232
+ err instanceof Error ? err.message : err,
233
+ );
234
+ process.exit(1);
235
+ }
236
+ });
237
+
238
+ program
239
+ .command("config")
240
+ .description("Show configuration")
241
+ .action(() => {
242
+ try {
243
+ const config = loadConfig();
244
+ console.log("\x1b[36mCurrent Configuration:\x1b[0m");
245
+ console.log(JSON.stringify(config, null, 2));
246
+ } catch (err) {
247
+ console.error(
248
+ "\x1b[31mError:\x1b[0m",
249
+ err instanceof Error ? err.message : err,
250
+ );
251
+ }
252
+ });
253
+
254
+ program
255
+ .command("providers")
256
+ .description("List available providers")
257
+ .action(() => {
258
+ try {
259
+ const config = loadConfig();
260
+ const providerRegistry = new ProviderRegistry();
261
+ providerRegistry.createFromConfig(config);
262
+ console.log(
263
+ "\x1b[36mAvailable providers:\x1b[0m",
264
+ providerRegistry.list().join(", "),
265
+ );
266
+ } catch (err) {
267
+ console.error(
268
+ "\x1b[31mError:\x1b[0m",
269
+ err instanceof Error ? err.message : err,
270
+ );
271
+ }
272
+ });
273
+
274
+ program
275
+ .command("skills")
276
+ .description("List available skills")
277
+ .action(() => {
278
+ try {
279
+ const skillRegistry = new SkillRegistry();
280
+ const builtinSkills = createBuiltinSkills();
281
+ builtinSkills.forEach((s) => skillRegistry.register(s));
282
+
283
+ console.log("\x1b[36mAvailable Skills:\x1b[0m\n");
284
+ for (const skill of skillRegistry.list()) {
285
+ console.log(`\x1b[32m${skill.name}\x1b[0m: ${skill.description}`);
286
+ if (skill.triggers && skill.triggers.length > 0) {
287
+ console.log(
288
+ ` Triggers: ${skill.triggers.map((t) => t.value).join(", ")}`,
289
+ );
290
+ }
291
+ console.log();
292
+ }
293
+ } catch (err) {
294
+ console.error(
295
+ "\x1b[31mError:\x1b[0m",
296
+ err instanceof Error ? err.message : err,
297
+ );
298
+ }
299
+ });
300
+
301
+ program
302
+ .command("doctor")
303
+ .description("Check configuration and environment")
304
+ .action(() => {
305
+ console.log("\x1b[36mHalfCopilot Doctor\x1b[0m\n");
306
+
307
+ try {
308
+ const config = loadConfig();
309
+ console.log("\x1b[32m✓\x1b[0m Configuration loaded successfully");
310
+
311
+ const providerRegistry = new ProviderRegistry();
312
+ providerRegistry.createFromConfig(config);
313
+ console.log(
314
+ `\x1b[32m✓\x1b[0m Providers: ${providerRegistry.list().join(", ")}`,
315
+ );
316
+
317
+ console.log(
318
+ `\x1b[32m✓\x1b[0m Default provider: ${config.defaultProvider}`,
319
+ );
320
+ console.log(`\x1b[32m✓\x1b[0m Default model: ${config.defaultModel}`);
321
+
322
+ const toolRegistry = new ToolRegistry();
323
+ const builtinTools = createBuiltinTools();
324
+ builtinTools.forEach((t) => toolRegistry.register(t));
325
+ console.log(
326
+ `\x1b[32m✓\x1b[0m Built-in tools: ${toolRegistry.list().join(", ")}`,
327
+ );
328
+
329
+ const skillRegistry = new SkillRegistry();
330
+ const builtinSkills = createBuiltinSkills();
331
+ builtinSkills.forEach((s) => skillRegistry.register(s));
332
+ console.log(
333
+ `\x1b[32m✓\x1b[0m Skills: ${skillRegistry
334
+ .list()
335
+ .map((s) => s.name)
336
+ .join(", ")}`,
337
+ );
338
+
339
+ console.log("\n\x1b[32mAll checks passed!\x1b[0m");
340
+ } catch (err) {
341
+ console.error(
342
+ "\x1b[31m✗ Error:\x1b[0m",
343
+ err instanceof Error ? err.message : err,
344
+ );
345
+ }
346
+ });
347
+
348
+ program.parse();
@@ -0,0 +1,160 @@
1
+ import React, { useState, useCallback, useEffect } from "react";
2
+ import { Box, Text, useApp } from "ink";
3
+ import {
4
+ StatusBar,
5
+ ChatView,
6
+ InputField,
7
+ ToolApproval,
8
+ } from "./components/index.js";
9
+ import type { AgentLoop, AgentEvent } from "@halfcopilot/core";
10
+
11
+ interface Message {
12
+ role: "user" | "assistant" | "system" | "tool";
13
+ content: string;
14
+ toolName?: string;
15
+ }
16
+
17
+ interface AppProps {
18
+ agent: AgentLoop;
19
+ providerName: string;
20
+ model: string;
21
+ mode: string;
22
+ }
23
+
24
+ export const App: React.FC<AppProps> = ({
25
+ agent,
26
+ providerName,
27
+ model,
28
+ mode,
29
+ }) => {
30
+ const { exit } = useApp();
31
+ const [messages, setMessages] = useState<Message[]>([]);
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const [currentMode, setCurrentMode] = useState(mode);
34
+ const [tokenUsage, setTokenUsage] = useState({
35
+ inputTokens: 0,
36
+ outputTokens: 0,
37
+ });
38
+ const [turnCount, setTurnCount] = useState(0);
39
+ const [lastResponseTime, setLastResponseTime] = useState<number>(0);
40
+ const [pendingApproval, setPendingApproval] = useState<{
41
+ toolName: string;
42
+ toolInput: Record<string, unknown>;
43
+ permissionLevel?: "SAFE" | "WARN" | "UNSAFE";
44
+ } | null>(null);
45
+
46
+ const handleSubmit = useCallback(
47
+ async (input: string) => {
48
+ if (input.toLowerCase() === "exit" || input.toLowerCase() === "quit") {
49
+ exit();
50
+ return;
51
+ }
52
+
53
+ setMessages((prev) => [...prev, { role: "user", content: input }]);
54
+ setIsLoading(true);
55
+ setTurnCount((prev) => prev + 1);
56
+
57
+ try {
58
+ let fullText = "";
59
+ const respStart = Date.now();
60
+ for await (const event of agent.run(input)) {
61
+ switch (event.type) {
62
+ case "text":
63
+ fullText += event.content ?? "";
64
+ break;
65
+ case "tool_use":
66
+ setMessages((prev) => [
67
+ ...prev,
68
+ {
69
+ role: "tool",
70
+ content: JSON.stringify(event.toolInput),
71
+ toolName: event.toolName ?? "",
72
+ },
73
+ ]);
74
+ setPendingApproval({
75
+ toolName: event.toolName ?? "",
76
+ toolInput: event.toolInput ?? {},
77
+ permissionLevel: "WARN",
78
+ });
79
+ break;
80
+ case "tool_result":
81
+ setPendingApproval(null);
82
+ setMessages((prev) => [
83
+ ...prev,
84
+ {
85
+ role: "tool",
86
+ content: event.toolOutput ?? "",
87
+ toolName: event.toolName,
88
+ },
89
+ ]);
90
+ break;
91
+ case "done":
92
+ setLastResponseTime(Date.now() - respStart);
93
+ if (event.usage) {
94
+ setTokenUsage((prev) => ({
95
+ inputTokens: prev.inputTokens + event.usage!.inputTokens,
96
+ outputTokens: prev.outputTokens + event.usage!.outputTokens,
97
+ }));
98
+ }
99
+ break;
100
+ case "error":
101
+ setMessages((prev) => [
102
+ ...prev,
103
+ {
104
+ role: "system",
105
+ content: `Error: ${event.error?.message}`,
106
+ },
107
+ ]);
108
+ break;
109
+ }
110
+ }
111
+
112
+ if (fullText) {
113
+ setMessages((prev) => [
114
+ ...prev,
115
+ { role: "assistant", content: fullText },
116
+ ]);
117
+ }
118
+ } catch (err) {
119
+ setMessages((prev) => [
120
+ ...prev,
121
+ {
122
+ role: "system",
123
+ content: `Error: ${err instanceof Error ? err.message : err}`,
124
+ },
125
+ ]);
126
+ } finally {
127
+ setIsLoading(false);
128
+ setPendingApproval(null);
129
+ }
130
+ },
131
+ [agent, exit],
132
+ );
133
+
134
+ return (
135
+ <Box flexDirection="column" height="100%">
136
+ <StatusBar
137
+ provider={providerName}
138
+ model={model}
139
+ mode={currentMode}
140
+ tokenUsage={tokenUsage}
141
+ turnInfo={{ current: turnCount, max: 20 }}
142
+ responseTime={lastResponseTime}
143
+ />
144
+ <Box flexDirection="column" flexGrow={1}>
145
+ <ChatView messages={messages} />
146
+ </Box>
147
+ {pendingApproval ? (
148
+ <ToolApproval
149
+ toolName={pendingApproval.toolName}
150
+ toolInput={pendingApproval.toolInput}
151
+ permissionLevel={pendingApproval.permissionLevel}
152
+ onApprove={() => setPendingApproval(null)}
153
+ onReject={() => setPendingApproval(null)}
154
+ />
155
+ ) : (
156
+ <InputField onSubmit={handleSubmit} isLoading={isLoading} />
157
+ )}
158
+ </Box>
159
+ );
160
+ };
@@ -0,0 +1,130 @@
1
+ import React, { Fragment } from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ interface Message {
5
+ role: "user" | "assistant" | "system" | "tool";
6
+ content: string;
7
+ toolName?: string;
8
+ }
9
+
10
+ interface ChatViewProps {
11
+ messages: Message[];
12
+ }
13
+
14
+ function CodeBlock({ code, language }: { code: string; language?: string }) {
15
+ const lines = code.split("\n");
16
+ return (
17
+ <Box flexDirection="column" marginLeft={2} marginY={0}>
18
+ {language && (
19
+ <Box>
20
+ <Text color="gray">{language}</Text>
21
+ </Box>
22
+ )}
23
+ {lines.map((line, i) => (
24
+ <Box key={i}>
25
+ <Text color="gray">│ </Text>
26
+ <Text color="white">{line}</Text>
27
+ </Box>
28
+ ))}
29
+ </Box>
30
+ );
31
+ }
32
+
33
+ function formatContent(content: string): React.ReactNode[] {
34
+ const parts = content.split(/(```[\s\S]*?```)/);
35
+ const nodes: React.ReactNode[] = [];
36
+ let key = 0;
37
+
38
+ for (const part of parts) {
39
+ if (part.startsWith("```")) {
40
+ const code = part
41
+ .replace(/```(\w*)\n?/, (_, lang) => {
42
+ nodes.push(<CodeBlock key={key++} code={""} language={lang} />);
43
+ return "";
44
+ })
45
+ .replace(/```$/, "");
46
+ const match = part.match(/```(\w*)\n?([\s\S]*?)```/);
47
+ if (match) {
48
+ nodes.push(
49
+ <CodeBlock
50
+ key={key++}
51
+ code={match[2]}
52
+ language={match[1] || undefined}
53
+ />,
54
+ );
55
+ }
56
+ } else if (part) {
57
+ nodes.push(<Text key={key++}>{part}</Text>);
58
+ }
59
+ }
60
+
61
+ if (nodes.length === 0) {
62
+ nodes.push(<Text key={0}>{content}</Text>);
63
+ }
64
+ return nodes;
65
+ }
66
+
67
+ export const ChatView: React.FC<ChatViewProps> = ({ messages }) => {
68
+ // Group consecutive same-role messages
69
+ const grouped: { role: Message["role"]; contents: Message[] }[] = [];
70
+ for (const msg of messages) {
71
+ const last = grouped[grouped.length - 1];
72
+ if (last && last.role === msg.role && msg.role !== "tool") {
73
+ last.contents.push(msg);
74
+ } else {
75
+ grouped.push({ role: msg.role, contents: [msg] });
76
+ }
77
+ }
78
+
79
+ return (
80
+ <Box flexDirection="column" padding={1}>
81
+ {grouped.map((group, gi) => (
82
+ <Box key={gi} marginBottom={1} flexDirection="column">
83
+ {group.role === "user" && (
84
+ <Box>
85
+ <Text color="green" bold>
86
+ {"❯ "}
87
+ </Text>
88
+ <Text color="white">{group.contents[0].content}</Text>
89
+ </Box>
90
+ )}
91
+ {group.role === "assistant" && (
92
+ <Box flexDirection="column">
93
+ <Box marginBottom={0}>
94
+ <Text color="blue" bold>
95
+ {"● "}
96
+ </Text>
97
+ {group.contents.map((msg, i) => (
98
+ <Fragment key={i}>{formatContent(msg.content)}</Fragment>
99
+ ))}
100
+ </Box>
101
+ </Box>
102
+ )}
103
+ {group.role === "tool" && (
104
+ <Box flexDirection="column">
105
+ {group.contents.map((msg, i) => (
106
+ <Box key={i}>
107
+ <Text color="cyan" bold>
108
+ {"🔧 "}
109
+ </Text>
110
+ <Text color="cyan">[{msg.toolName}] </Text>
111
+ <Text color="gray">
112
+ {msg.content.slice(0, 120)}
113
+ {msg.content.length > 120 ? "..." : ""}
114
+ </Text>
115
+ </Box>
116
+ ))}
117
+ </Box>
118
+ )}
119
+ {group.role === "system" && (
120
+ <Box>
121
+ <Text color="gray">
122
+ {group.contents.map((m) => m.content).join("\n")}
123
+ </Text>
124
+ </Box>
125
+ )}
126
+ </Box>
127
+ ))}
128
+ </Box>
129
+ );
130
+ };
@@ -0,0 +1,41 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+
4
+ interface InputFieldProps {
5
+ onSubmit: (input: string) => void;
6
+ isLoading?: boolean;
7
+ }
8
+
9
+ export const InputField: React.FC<InputFieldProps> = ({
10
+ onSubmit,
11
+ isLoading,
12
+ }) => {
13
+ const [value, setValue] = useState("");
14
+
15
+ useInput((input, key) => {
16
+ if (key.return) {
17
+ if (value.trim()) {
18
+ onSubmit(value.trim());
19
+ setValue("");
20
+ }
21
+ } else if (key.backspace || key.delete) {
22
+ setValue((prev) => prev.slice(0, -1));
23
+ } else if (input && !key.ctrl && !key.meta) {
24
+ setValue((prev) => prev + input);
25
+ }
26
+ });
27
+
28
+ return (
29
+ <Box borderStyle="single" borderColor="green" paddingX={1}>
30
+ <Text color="green" bold>
31
+ {"❯ "}
32
+ </Text>
33
+ <Text color="white">{value}</Text>
34
+ {isLoading ? (
35
+ <Text color="yellow"> ⏳</Text>
36
+ ) : (
37
+ <Text color="gray">█</Text>
38
+ )}
39
+ </Box>
40
+ );
41
+ };