thatgfsj-code 0.4.1 → 0.6.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 (94) hide show
  1. package/README.md +7 -12
  2. package/dist/app/index.d.ts +6 -14
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +29 -31
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cmd/index.d.ts +1 -2
  7. package/dist/cmd/index.d.ts.map +1 -1
  8. package/dist/cmd/index.js +58 -47
  9. package/dist/cmd/index.js.map +1 -1
  10. package/dist/config/index.d.ts.map +1 -1
  11. package/dist/config/index.js +1 -0
  12. package/dist/config/index.js.map +1 -1
  13. package/dist/config/types.d.ts +1 -0
  14. package/dist/config/types.d.ts.map +1 -1
  15. package/dist/llm/index.d.ts.map +1 -1
  16. package/dist/llm/index.js +0 -1
  17. package/dist/llm/index.js.map +1 -1
  18. package/dist/tui/app.d.ts +9 -0
  19. package/dist/tui/app.d.ts.map +1 -0
  20. package/dist/tui/app.js +40 -0
  21. package/dist/tui/app.js.map +1 -0
  22. package/dist/tui/components/ChatList.d.ts +14 -0
  23. package/dist/tui/components/ChatList.d.ts.map +1 -0
  24. package/dist/tui/components/ChatList.js +11 -0
  25. package/dist/tui/components/ChatList.js.map +1 -0
  26. package/dist/tui/components/ChatMessage.d.ts +14 -0
  27. package/dist/tui/components/ChatMessage.d.ts.map +1 -0
  28. package/dist/tui/components/ChatMessage.js +17 -0
  29. package/dist/tui/components/ChatMessage.js.map +1 -0
  30. package/dist/tui/components/Header.d.ts +9 -0
  31. package/dist/tui/components/Header.d.ts.map +1 -0
  32. package/dist/tui/components/Header.js +6 -0
  33. package/dist/tui/components/Header.js.map +1 -0
  34. package/dist/tui/components/Markdown.d.ts +8 -0
  35. package/dist/tui/components/Markdown.d.ts.map +1 -0
  36. package/dist/tui/components/Markdown.js +31 -0
  37. package/dist/tui/components/Markdown.js.map +1 -0
  38. package/dist/tui/components/Thinking.d.ts +8 -0
  39. package/dist/tui/components/Thinking.d.ts.map +1 -0
  40. package/dist/tui/components/Thinking.js +7 -0
  41. package/dist/tui/components/Thinking.js.map +1 -0
  42. package/dist/tui/components/ToolCall.d.ts +14 -0
  43. package/dist/tui/components/ToolCall.d.ts.map +1 -0
  44. package/dist/tui/components/ToolCall.js +37 -0
  45. package/dist/tui/components/ToolCall.js.map +1 -0
  46. package/dist/tui/components/UserInput.d.ts +9 -0
  47. package/dist/tui/components/UserInput.d.ts.map +1 -0
  48. package/dist/tui/components/UserInput.js +82 -0
  49. package/dist/tui/components/UserInput.js.map +1 -0
  50. package/dist/tui/hooks/useChat.d.ts +11 -0
  51. package/dist/tui/hooks/useChat.d.ts.map +1 -0
  52. package/dist/tui/hooks/useChat.js +138 -0
  53. package/dist/tui/hooks/useChat.js.map +1 -0
  54. package/dist/tui/hooks/useCommands.d.ts +10 -0
  55. package/dist/tui/hooks/useCommands.d.ts.map +1 -0
  56. package/dist/tui/hooks/useCommands.js +51 -0
  57. package/dist/tui/hooks/useCommands.js.map +1 -0
  58. package/dist/tui/index.d.ts +2 -4
  59. package/dist/tui/index.d.ts.map +1 -1
  60. package/dist/tui/index.js +2 -4
  61. package/dist/tui/index.js.map +1 -1
  62. package/dist/tui/output.d.ts +12 -3
  63. package/dist/tui/output.d.ts.map +1 -1
  64. package/dist/tui/output.js +43 -47
  65. package/dist/tui/output.js.map +1 -1
  66. package/dist/tui/repl.d.ts.map +1 -1
  67. package/dist/tui/repl.js +42 -28
  68. package/dist/tui/repl.js.map +1 -1
  69. package/dist/tui/welcome.d.ts.map +1 -1
  70. package/dist/tui/welcome.js +23 -5
  71. package/dist/tui/welcome.js.map +1 -1
  72. package/install.ps1 +171 -192
  73. package/package.json +9 -1
  74. package/src/app/index.ts +29 -34
  75. package/src/cmd/{index.ts → index.tsx} +52 -47
  76. package/src/config/index.ts +1 -0
  77. package/src/config/types.ts +1 -0
  78. package/src/llm/index.ts +0 -1
  79. package/src/tui/app.tsx +60 -0
  80. package/src/tui/components/ChatList.tsx +29 -0
  81. package/src/tui/components/ChatMessage.tsx +51 -0
  82. package/src/tui/components/Header.tsx +22 -0
  83. package/src/tui/components/Markdown.tsx +35 -0
  84. package/src/tui/components/Thinking.tsx +19 -0
  85. package/src/tui/components/ToolCall.tsx +72 -0
  86. package/src/tui/components/UserInput.tsx +101 -0
  87. package/src/tui/hooks/useChat.ts +154 -0
  88. package/src/tui/hooks/useCommands.ts +63 -0
  89. package/src/tui/index.ts +2 -4
  90. package/src/tui/output.ts +43 -44
  91. package/src/tui/repl.ts +40 -29
  92. package/src/tui/welcome.ts +25 -6
  93. package/tsconfig.json +5 -3
  94. package/src/app/agent.ts +0 -140
@@ -2,10 +2,9 @@
2
2
 
3
3
  /**
4
4
  * Thatgfsj Code - CLI Entry Point
5
- * Simplified from old src/index.ts (655 lines → ~60 lines)
6
5
  *
7
6
  * Two modes:
8
- * gfcode → Interactive REPL
7
+ * gfcode → Interactive TUI (Ink)
9
8
  * gfcode "prompt" → Single prompt mode
10
9
  * gfcode init → Setup wizard
11
10
  */
@@ -20,18 +19,18 @@ if (process.platform === 'win32') {
20
19
  import { program } from 'commander';
21
20
  import chalk from 'chalk';
22
21
  import { App } from '../app/index.js';
23
- import { REPL } from '../tui/repl.js';
24
22
  import { WelcomeScreen } from '../tui/welcome.js';
23
+ import { REPLOutput } from '../tui/output.js';
25
24
 
26
25
  // ==================== Error Handling ====================
27
26
 
28
27
  process.on('uncaughtException', (error) => {
29
- console.error(chalk.red('\nError:'), error.message);
28
+ console.error(chalk.red('\n Error:'), error.message);
30
29
  process.exit(1);
31
30
  });
32
31
 
33
32
  process.on('unhandledRejection', (reason) => {
34
- console.error(chalk.red('\nError:'), reason);
33
+ console.error(chalk.red('\n Error:'), reason);
35
34
  process.exit(1);
36
35
  });
37
36
 
@@ -39,15 +38,13 @@ process.on('unhandledRejection', (reason) => {
39
38
 
40
39
  program
41
40
  .name('gfcode')
42
- .description('🤖 Thatgfsj Code - AI Coding Assistant')
43
- .version('0.4.1')
41
+ .description('Thatgfsj Code - AI Coding Assistant')
42
+ .version('0.6.0')
44
43
  .argument('[prompt]', 'Task to execute (omit to start interactive mode)')
45
44
  .option('-m, --model <model>', 'Specify model')
46
45
  .option('-i, --interactive', 'Force interactive mode')
47
- .option('--hooks', 'Enable audit logging hooks')
48
- .action(async (prompt: string | undefined, options: { model?: string; interactive?: boolean; hooks?: boolean }) => {
46
+ .action(async (prompt: string | undefined, options: { model?: string; interactive?: boolean }) => {
49
47
  try {
50
- // Initialize app
51
48
  const app = await App.create();
52
49
 
53
50
  // Show welcome if no API key
@@ -58,21 +55,27 @@ program
58
55
  await app.config.save({ model: options.model });
59
56
  }
60
57
 
61
- // Enable hooks if requested
62
- if (options.hooks) {
63
- const { auditLogHook } = await import('../hooks/index.js');
64
- app.hooks.register('afterToolCall', auditLogHook);
65
- }
66
-
67
58
  if (!prompt || options.interactive) {
68
- // Interactive REPL mode
69
- const repl = new REPL(app);
70
- await repl.start();
59
+ // Interactive mode - use Ink TUI
60
+ const { render } = await import('ink');
61
+ const { TuiApp } = await import('../tui/app.js');
62
+
63
+ const { unmount } = render(<TuiApp app={app} />);
64
+
65
+ // Wait for the app to exit
66
+ await new Promise<void>((resolve) => {
67
+ const check = setInterval(() => {
68
+ // Ink will call process.exit or we detect it
69
+ }, 1000);
70
+ process.on('exit', () => {
71
+ clearInterval(check);
72
+ unmount();
73
+ resolve();
74
+ });
75
+ });
71
76
  } else {
72
- // Single prompt mode - use same UI as REPL
73
- const { REPLOutput } = await import('../tui/output.js');
77
+ // Single prompt mode - use simple output
74
78
  const out = new REPLOutput();
75
-
76
79
  out.printBanner();
77
80
  out.printUserInput(prompt);
78
81
  out.startThinking();
@@ -82,31 +85,34 @@ program
82
85
  let hasStarted = false;
83
86
 
84
87
  try {
85
- const stream = app.getAgent().run(app.session.getMessages());
88
+ const stream = app.streamResponse();
86
89
  for await (const chunk of stream) {
87
90
  out.stopThinking();
88
91
 
89
- const parts = chunk.split('\n');
90
- for (const part of parts) {
91
- if (part.startsWith('@@TOOL@@')) {
92
- try {
93
- const data = JSON.parse(part.slice(8));
94
- if (data.action === 'call') {
95
- if (hasStarted) { out.endAssistant(); hasStarted = false; }
96
- out.printToolCall(data.name, data.args || '');
97
- out.startExecuting(data.name);
98
- } else if (data.action === 'result') {
99
- out.stopThinking();
100
- out.printToolResult(data.output || data.error || '', !!data.error);
101
- out.printToolEnd();
102
- }
103
- } catch {}
104
- continue;
105
- }
106
- if (part) {
107
- if (!hasStarted) { out.beginAssistant(); hasStarted = true; }
108
- out.writeChunk(part + '\n');
92
+ if (chunk.includes('@@TOOL@@')) {
93
+ const parts = chunk.split('\n');
94
+ for (const part of parts) {
95
+ if (part.startsWith('@@TOOL@@')) {
96
+ try {
97
+ const data = JSON.parse(part.slice(8));
98
+ if (data.action === 'call') {
99
+ if (hasStarted) { out.endAssistant(); hasStarted = false; }
100
+ out.printToolCall(data.name, data.args || '');
101
+ out.startExecuting(data.name);
102
+ } else if (data.action === 'result') {
103
+ out.stopThinking();
104
+ out.printToolResult(data.output || data.error || '', !!data.error);
105
+ out.printToolEnd();
106
+ }
107
+ } catch {}
108
+ } else if (part) {
109
+ if (!hasStarted) { out.beginAssistant(); hasStarted = true; }
110
+ out.writeChunk(part + '\n');
111
+ }
109
112
  }
113
+ } else {
114
+ if (!hasStarted) { out.beginAssistant(); hasStarted = true; }
115
+ out.writeChunk(chunk);
110
116
  }
111
117
  fullResponse += chunk;
112
118
  }
@@ -120,18 +126,17 @@ program
120
126
  app.session.addMessage('assistant', fullResponse);
121
127
  }
122
128
  } catch (error: any) {
123
- console.error(chalk.red(`\n${error.message}`));
129
+ console.error(chalk.red(`\n ${error.message}`));
124
130
  process.exit(1);
125
131
  }
126
132
  });
127
133
 
128
- // Init command - setup wizard
134
+ // Init command
129
135
  program
130
136
  .command('init')
131
- .description('配置 API Key 和模型')
137
+ .description('Configure API key and model')
132
138
  .action(async () => {
133
139
  await WelcomeScreen.interactiveSetup();
134
140
  });
135
141
 
136
- // Parse and run
137
142
  program.parse(process.argv);
@@ -14,6 +14,7 @@ const DEFAULT_CONFIG: Config = {
14
14
  apiKey: '',
15
15
  temperature: 0.7,
16
16
  maxTokens: 4096,
17
+ contextLength: 50,
17
18
  provider: 'siliconflow',
18
19
  };
19
20
 
@@ -32,6 +32,7 @@ export interface Config {
32
32
  apiKey: string;
33
33
  temperature: number;
34
34
  maxTokens: number;
35
+ contextLength: number;
35
36
  provider: ProviderName;
36
37
  baseUrl?: string;
37
38
  }
package/src/llm/index.ts CHANGED
@@ -91,7 +91,6 @@ export class LLMService {
91
91
  let fullContent = '';
92
92
  let detectedToolCalls: ToolCall[] | undefined;
93
93
 
94
- // Stream the response, passing tools so the API knows about them
95
94
  const stream = this.provider.chatStream(currentMessages, options, hasTools ? toolsArray : undefined);
96
95
 
97
96
  for await (const chunk of stream) {
@@ -0,0 +1,60 @@
1
+ /** @jsxImportSource react */
2
+ import React, { useState, useCallback, useEffect } from 'react';
3
+ import { Box, Text, useApp, useStdout } from 'ink';
4
+ import { Header } from './components/Header.js';
5
+ import { ChatList } from './components/ChatList.js';
6
+ import { Thinking } from './components/Thinking.js';
7
+ import { UserInput } from './components/UserInput.js';
8
+ import { useChat } from './hooks/useChat.js';
9
+ import { useCommands } from './hooks/useCommands.js';
10
+ import type { App } from '../app/index.js';
11
+ import type { MessageData } from './components/ChatMessage.js';
12
+
13
+ interface Props {
14
+ app: App;
15
+ }
16
+
17
+ export function TuiApp({ app }: Props) {
18
+ const config = app.config.get();
19
+ const { messages, isThinking, streaming, streamingToolCalls, sendMessage } = useChat(app);
20
+ const { handleCommand } = useCommands(app);
21
+ const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
22
+ const { exit } = useApp();
23
+
24
+ const onSubmit = useCallback(async (input: string) => {
25
+ // Check for built-in commands first
26
+ const result = handleCommand(input);
27
+ if (result.handled) {
28
+ if (result.output) {
29
+ setSystemMessages(prev => [
30
+ ...prev,
31
+ { role: 'user', content: input },
32
+ { role: 'assistant', content: result.output! },
33
+ ]);
34
+ }
35
+ // Handle clear
36
+ if (input.toLowerCase().trim() === 'clear') {
37
+ setSystemMessages([]);
38
+ }
39
+ return;
40
+ }
41
+
42
+ // Send to AI
43
+ await sendMessage(input);
44
+ }, [handleCommand, sendMessage]);
45
+
46
+ const allMessages = [...systemMessages, ...messages];
47
+
48
+ return (
49
+ <Box flexDirection="column" padding={1}>
50
+ <Header provider={config.provider} model={config.model} />
51
+ <ChatList
52
+ messages={allMessages}
53
+ streaming={streaming}
54
+ streamingToolCalls={streamingToolCalls}
55
+ />
56
+ {isThinking && <Thinking />}
57
+ <UserInput onSubmit={onSubmit} disabled={isThinking} />
58
+ </Box>
59
+ );
60
+ }
@@ -0,0 +1,29 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box } from 'ink';
4
+ import { ChatMessage, type MessageData } from './ChatMessage.js';
5
+
6
+ interface Props {
7
+ messages: MessageData[];
8
+ streaming?: string;
9
+ streamingToolCalls?: Array<{ name: string; args: string }>;
10
+ }
11
+
12
+ export function ChatList({ messages, streaming, streamingToolCalls }: Props) {
13
+ return (
14
+ <Box flexDirection="column" flexGrow={1}>
15
+ {messages.map((msg, i) => (
16
+ <ChatMessage key={i} message={msg} />
17
+ ))}
18
+ {(streaming || streamingToolCalls) && (
19
+ <ChatMessage
20
+ message={{
21
+ role: 'assistant',
22
+ content: streaming || '',
23
+ toolCalls: streamingToolCalls,
24
+ }}
25
+ />
26
+ )}
27
+ </Box>
28
+ );
29
+ }
@@ -0,0 +1,51 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { Markdown } from './Markdown.js';
5
+ import { ToolCall, type ToolCallData } from './ToolCall.js';
6
+
7
+ interface MessageData {
8
+ role: 'user' | 'assistant' | 'tool';
9
+ content: string;
10
+ toolCalls?: ToolCallData[];
11
+ }
12
+
13
+ interface Props {
14
+ message: MessageData;
15
+ }
16
+
17
+ function UserMessage({ content }: { content: string }) {
18
+ return (
19
+ <Box flexDirection="column" marginBottom={1}>
20
+ <Text bold>You</Text>
21
+ <Box paddingLeft={2}>
22
+ <Text>{content}</Text>
23
+ </Box>
24
+ </Box>
25
+ );
26
+ }
27
+
28
+ function AssistantMessage({ content, toolCalls }: { content: string; toolCalls?: ToolCallData[] }) {
29
+ return (
30
+ <Box flexDirection="column" marginBottom={1}>
31
+ <Text bold color="cyan">AI</Text>
32
+ {toolCalls && toolCalls.map((tc, i) => (
33
+ <ToolCall key={i} tool={tc} />
34
+ ))}
35
+ {content && (
36
+ <Box paddingLeft={2} flexDirection="column">
37
+ <Markdown content={content} />
38
+ </Box>
39
+ )}
40
+ </Box>
41
+ );
42
+ }
43
+
44
+ export function ChatMessage({ message }: Props) {
45
+ if (message.role === 'user') {
46
+ return <UserMessage content={message.content} />;
47
+ }
48
+ return <AssistantMessage content={message.content} toolCalls={message.toolCalls} />;
49
+ }
50
+
51
+ export type { MessageData };
@@ -0,0 +1,22 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+
5
+ interface Props {
6
+ provider: string;
7
+ model: string;
8
+ }
9
+
10
+ export function Header({ provider, model }: Props) {
11
+ return (
12
+ <Box flexDirection="column" marginBottom={1}>
13
+ <Box>
14
+ <Text bold color="cyan"> ⚡ Thatgfsj Code </Text>
15
+ <Text dimColor> v0.6.0</Text>
16
+ <Text dimColor> │ </Text>
17
+ <Text dimColor>{provider} / {model}</Text>
18
+ </Box>
19
+ <Text dimColor>{'─'.repeat(60)}</Text>
20
+ </Box>
21
+ );
22
+ }
@@ -0,0 +1,35 @@
1
+ /** @jsxImportSource react */
2
+ import React, { useMemo } from 'react';
3
+ import { Text } from 'ink';
4
+ import { marked } from 'marked';
5
+ import TerminalRenderer from 'marked-terminal';
6
+
7
+ // Configure marked to use terminal renderer
8
+ marked.setOptions({
9
+ // @ts-ignore - marked-terminal types are incomplete
10
+ renderer: new TerminalRenderer({
11
+ showSectionPrefix: false,
12
+ tab: 2,
13
+ }),
14
+ });
15
+
16
+ interface Props {
17
+ content: string;
18
+ }
19
+
20
+ export function Markdown({ content }: Props) {
21
+ const rendered = useMemo(() => {
22
+ try {
23
+ const result = marked.parse(content);
24
+ // marked.parse returns string | Promise<string> depending on async option
25
+ if (typeof result === 'string') {
26
+ return result.trimEnd();
27
+ }
28
+ return content;
29
+ } catch {
30
+ return content;
31
+ }
32
+ }, [content]);
33
+
34
+ return <Text>{rendered}</Text>;
35
+ }
@@ -0,0 +1,19 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+
6
+ interface Props {
7
+ message?: string;
8
+ }
9
+
10
+ export function Thinking({ message = 'Thinking' }: Props) {
11
+ return (
12
+ <Box>
13
+ <Text color="cyan">
14
+ <Spinner type="dots" />
15
+ </Text>
16
+ <Text dimColor> {message}</Text>
17
+ </Box>
18
+ );
19
+ }
@@ -0,0 +1,72 @@
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+
5
+ interface ToolCallData {
6
+ name: string;
7
+ args: string;
8
+ result?: string;
9
+ isError?: boolean;
10
+ }
11
+
12
+ interface Props {
13
+ tool: ToolCallData;
14
+ }
15
+
16
+ function formatArgs(args: string): string {
17
+ try {
18
+ const obj = JSON.parse(args);
19
+ const entries = Object.entries(obj);
20
+ if (entries.length === 0) return '';
21
+ return entries
22
+ .map(([k, v]) => {
23
+ const val = typeof v === 'string' && v.length > 60
24
+ ? v.slice(0, 60) + '...'
25
+ : JSON.stringify(v);
26
+ return `${k}=${val}`;
27
+ })
28
+ .join(' ');
29
+ } catch {
30
+ return args.length > 80 ? args.slice(0, 80) + '...' : args;
31
+ }
32
+ }
33
+
34
+ function formatResult(output: string, maxLines = 15): { text: string; truncated: boolean } {
35
+ const lines = output.split('\n');
36
+ if (lines.length <= maxLines) {
37
+ return { text: output, truncated: false };
38
+ }
39
+ return {
40
+ text: lines.slice(0, maxLines).join('\n'),
41
+ truncated: true,
42
+ };
43
+ }
44
+
45
+ export function ToolCall({ tool }: Props) {
46
+ const argsStr = formatArgs(tool.args);
47
+ const result = tool.result ? formatResult(tool.result) : null;
48
+
49
+ return (
50
+ <Box flexDirection="column" marginY={0}>
51
+ <Box>
52
+ <Text color="yellow"> ⚙ </Text>
53
+ <Text bold color="yellow">{tool.name}</Text>
54
+ {argsStr && <Text dimColor> {argsStr}</Text>}
55
+ </Box>
56
+ {result && (
57
+ <Box paddingLeft={4} flexDirection="column">
58
+ {result.text.split('\n').map((line, i) => (
59
+ <Text key={i} dimColor={tool.isError} color={tool.isError ? 'red' : undefined}>
60
+ {tool.isError ? '✖ ' : '│ '}{line}
61
+ </Text>
62
+ ))}
63
+ {result.truncated && (
64
+ <Text dimColor>│ ... (truncated)</Text>
65
+ )}
66
+ </Box>
67
+ )}
68
+ </Box>
69
+ );
70
+ }
71
+
72
+ export type { ToolCallData };
@@ -0,0 +1,101 @@
1
+ /** @jsxImportSource react */
2
+ import React, { useState, useCallback } from 'react';
3
+ import { Box, Text, useInput, useApp } from 'ink';
4
+
5
+ interface Props {
6
+ onSubmit: (input: string) => void;
7
+ disabled?: boolean;
8
+ }
9
+
10
+ export function UserInput({ onSubmit, disabled }: Props) {
11
+ const [value, setValue] = useState('');
12
+ const [cursorPos, setCursorPos] = useState(0);
13
+ const [history, setHistory] = useState<string[]>([]);
14
+ const [historyIdx, setHistoryIdx] = useState(-1);
15
+ const { exit } = useApp();
16
+
17
+ useInput((input, key) => {
18
+ if (disabled) return;
19
+
20
+ if (key.return) {
21
+ const trimmed = value.trim();
22
+ if (trimmed) {
23
+ if (trimmed === 'exit' || trimmed === 'quit') {
24
+ exit();
25
+ return;
26
+ }
27
+ setHistory(prev => [...prev, trimmed]);
28
+ setHistoryIdx(-1);
29
+ onSubmit(trimmed);
30
+ setValue('');
31
+ setCursorPos(0);
32
+ }
33
+ return;
34
+ }
35
+
36
+ if (key.upArrow) {
37
+ if (history.length > 0) {
38
+ const newIdx = historyIdx === -1 ? history.length - 1 : Math.max(0, historyIdx - 1);
39
+ setHistoryIdx(newIdx);
40
+ setValue(history[newIdx]);
41
+ setCursorPos(history[newIdx].length);
42
+ }
43
+ return;
44
+ }
45
+
46
+ if (key.downArrow) {
47
+ if (historyIdx >= 0) {
48
+ const newIdx = historyIdx + 1;
49
+ if (newIdx >= history.length) {
50
+ setHistoryIdx(-1);
51
+ setValue('');
52
+ setCursorPos(0);
53
+ } else {
54
+ setHistoryIdx(newIdx);
55
+ setValue(history[newIdx]);
56
+ setCursorPos(history[newIdx].length);
57
+ }
58
+ }
59
+ return;
60
+ }
61
+
62
+ if (key.backspace || key.delete) {
63
+ if (cursorPos > 0) {
64
+ setValue(v => v.slice(0, cursorPos - 1) + v.slice(cursorPos));
65
+ setCursorPos(p => p - 1);
66
+ }
67
+ return;
68
+ }
69
+
70
+ if (key.leftArrow) {
71
+ setCursorPos(p => Math.max(0, p - 1));
72
+ return;
73
+ }
74
+
75
+ if (key.rightArrow) {
76
+ setCursorPos(p => Math.min(value.length, p + 1));
77
+ return;
78
+ }
79
+
80
+ if (key.ctrl && input === 'c') {
81
+ exit();
82
+ return;
83
+ }
84
+
85
+ // Regular character input
86
+ if (input && !key.ctrl && !key.meta) {
87
+ setValue(v => v.slice(0, cursorPos) + input + v.slice(cursorPos));
88
+ setCursorPos(p => p + input.length);
89
+ }
90
+ });
91
+
92
+ const prefix = disabled ? ' ' : '❯ ';
93
+
94
+ return (
95
+ <Box>
96
+ <Text bold color="cyan">{prefix}</Text>
97
+ <Text>{value}</Text>
98
+ {!disabled && <Text color="cyan">█</Text>}
99
+ </Box>
100
+ );
101
+ }