thatgfsj-code 0.4.0 → 0.5.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 (53) hide show
  1. package/dist/app/index.d.ts +6 -14
  2. package/dist/app/index.d.ts.map +1 -1
  3. package/dist/app/index.js +28 -30
  4. package/dist/app/index.js.map +1 -1
  5. package/dist/cmd/index.js +64 -8
  6. package/dist/cmd/index.js.map +1 -1
  7. package/dist/llm/anthropic.d.ts +4 -17
  8. package/dist/llm/anthropic.d.ts.map +1 -1
  9. package/dist/llm/anthropic.js +54 -12
  10. package/dist/llm/anthropic.js.map +1 -1
  11. package/dist/llm/gemini.d.ts +3 -16
  12. package/dist/llm/gemini.d.ts.map +1 -1
  13. package/dist/llm/gemini.js +26 -22
  14. package/dist/llm/gemini.js.map +1 -1
  15. package/dist/llm/index.d.ts +5 -19
  16. package/dist/llm/index.d.ts.map +1 -1
  17. package/dist/llm/index.js +86 -87
  18. package/dist/llm/index.js.map +1 -1
  19. package/dist/llm/openai.d.ts +9 -23
  20. package/dist/llm/openai.d.ts.map +1 -1
  21. package/dist/llm/openai.js +48 -14
  22. package/dist/llm/openai.js.map +1 -1
  23. package/dist/llm/provider.d.ts +11 -16
  24. package/dist/llm/provider.d.ts.map +1 -1
  25. package/dist/tui/input.d.ts +0 -1
  26. package/dist/tui/input.d.ts.map +1 -1
  27. package/dist/tui/input.js +2 -5
  28. package/dist/tui/input.js.map +1 -1
  29. package/dist/tui/output.d.ts +36 -12
  30. package/dist/tui/output.d.ts.map +1 -1
  31. package/dist/tui/output.js +172 -40
  32. package/dist/tui/output.js.map +1 -1
  33. package/dist/tui/repl.d.ts +0 -10
  34. package/dist/tui/repl.d.ts.map +1 -1
  35. package/dist/tui/repl.js +91 -43
  36. package/dist/tui/repl.js.map +1 -1
  37. package/dist/tui/welcome.d.ts +1 -11
  38. package/dist/tui/welcome.d.ts.map +1 -1
  39. package/dist/tui/welcome.js +60 -56
  40. package/dist/tui/welcome.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/app/index.ts +28 -33
  43. package/src/cmd/index.ts +53 -8
  44. package/src/llm/anthropic.ts +60 -14
  45. package/src/llm/gemini.ts +31 -24
  46. package/src/llm/index.ts +93 -92
  47. package/src/llm/openai.ts +58 -15
  48. package/src/llm/provider.ts +12 -16
  49. package/src/tui/input.ts +2 -5
  50. package/src/tui/output.ts +192 -40
  51. package/src/tui/repl.ts +94 -44
  52. package/src/tui/welcome.ts +62 -59
  53. package/src/app/agent.ts +0 -140
package/src/tui/output.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * REPL Output Handler
3
- * Migrated from old src/repl/output.ts
2
+ * REPL Output - Clean terminal UI
3
+ * Inspired by opencode: simple indentation for streaming, clean boundaries
4
4
  */
5
5
 
6
6
  import chalk from 'chalk';
@@ -9,75 +9,227 @@ import ora, { Ora } from 'ora';
9
9
  export class REPLOutput {
10
10
  private spinner: Ora | null = null;
11
11
 
12
- printError(error: string): void {
13
- console.error(chalk.red(`\n❌ Error: ${error}`));
12
+ // ── Banner ──────────────────────────────────────────────
13
+
14
+ printBanner(): void {
15
+ console.log();
16
+ console.log(chalk.cyan.bold(' ⚡ Thatgfsj Code') + chalk.gray(' v0.5.0'));
17
+ console.log(chalk.gray(' AI Coding Assistant'));
18
+ console.log(chalk.gray(' ' + '─'.repeat(52)));
19
+ console.log();
20
+ }
21
+
22
+ // ── Prompt ──────────────────────────────────────────────
23
+
24
+ getPrompt(): string {
25
+ return chalk.cyan.bold('❯ ');
26
+ }
27
+
28
+ // ── User Input ──────────────────────────────────────────
29
+
30
+ printUserInput(input: string): void {
31
+ console.log();
32
+ console.log(chalk.bold(' You'));
33
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
34
+ for (const line of input.split('\n')) {
35
+ console.log(' ' + line);
36
+ }
37
+ console.log();
14
38
  }
15
39
 
16
- printWarning(warning: string): void {
17
- console.warn(chalk.yellow(`\n⚠️ ${warning}`));
40
+ // ── Assistant Streaming ─────────────────────────────────
41
+
42
+ /**
43
+ * Begin assistant response - just print the header
44
+ */
45
+ beginAssistant(): void {
46
+ console.log(chalk.bold(' AI'));
47
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
18
48
  }
19
49
 
20
- printSuccess(message: string): void {
21
- console.log(chalk.green(`\n✅ ${message}`));
50
+ /**
51
+ * Write streaming chunk - output as-is with indent.
52
+ * The chunk already contains proper newlines from the AI.
53
+ */
54
+ writeChunk(chunk: string): void {
55
+ // Indent each line by 2 spaces
56
+ const lines = chunk.split('\n');
57
+ for (let i = 0; i < lines.length; i++) {
58
+ if (lines[i]) {
59
+ process.stdout.write(' ' + lines[i]);
60
+ }
61
+ if (i < lines.length - 1) {
62
+ process.stdout.write('\n');
63
+ }
64
+ }
22
65
  }
23
66
 
24
- printInfo(message: string): void {
25
- console.log(chalk.gray(message));
67
+ /**
68
+ * End assistant response
69
+ */
70
+ endAssistant(): void {
71
+ process.stdout.write('\n');
72
+ console.log();
73
+ }
74
+
75
+ // ── Tool Calls ──────────────────────────────────────────
76
+
77
+ printToolCall(name: string, args: string): void {
78
+ console.log();
79
+ console.log(chalk.yellow(' ⚙ ') + chalk.bold(name));
80
+
81
+ if (args) {
82
+ try {
83
+ const obj = JSON.parse(args);
84
+ for (const [key, val] of Object.entries(obj)) {
85
+ let display: string;
86
+ if (typeof val === 'string') {
87
+ display = val.length > 100 ? val.slice(0, 100) + '...' : val;
88
+ } else {
89
+ display = JSON.stringify(val);
90
+ if (display.length > 100) display = display.slice(0, 100) + '...';
91
+ }
92
+ console.log(chalk.gray(' ' + key + ': ') + display);
93
+ }
94
+ } catch {
95
+ const display = args.length > 120 ? args.slice(0, 120) + '...' : args;
96
+ console.log(chalk.gray(' ') + display);
97
+ }
98
+ }
99
+ }
100
+
101
+ printToolResult(output: string, isError = false): void {
102
+ if (!output) return;
103
+
104
+ const lines = output.split('\n');
105
+ const maxLines = 20;
106
+ const display = lines.slice(0, maxLines);
107
+
108
+ for (const line of display) {
109
+ if (isError) {
110
+ console.log(chalk.red(' ✖ ') + line);
111
+ } else {
112
+ console.log(chalk.gray(' │ ') + line);
113
+ }
114
+ }
115
+
116
+ if (lines.length > maxLines) {
117
+ console.log(chalk.gray(' │ ') + chalk.dim(`... +${lines.length - maxLines} more lines`));
118
+ }
26
119
  }
27
120
 
28
- printDivider(char = '─', length = 40): void {
29
- console.log(chalk.gray(char.repeat(length)));
121
+ printToolEnd(): void {
122
+ console.log();
30
123
  }
31
124
 
32
- printHeader(text: string): void {
33
- this.printDivider();
34
- console.log(chalk.cyan(text));
35
- this.printDivider();
125
+ // ── Thinking ────────────────────────────────────────────
126
+
127
+ startThinking(): void {
128
+ this.spinner = ora({
129
+ text: chalk.gray('Thinking'),
130
+ color: 'cyan',
131
+ spinner: 'dots',
132
+ }).start();
36
133
  }
37
134
 
38
- startSpinner(text: string): void {
39
- this.spinner = ora({ text, color: 'cyan', spinner: 'dots' }).start();
135
+ startExecuting(toolName: string): void {
136
+ this.spinner = ora({
137
+ text: chalk.gray(`Executing ${chalk.yellow(toolName)}`),
138
+ color: 'cyan',
139
+ spinner: 'dots',
140
+ }).start();
40
141
  }
41
142
 
42
- stopSpinner(): void {
143
+ stopThinking(): void {
43
144
  if (this.spinner) {
44
145
  this.spinner.stop();
45
146
  this.spinner = null;
46
147
  }
47
148
  }
48
149
 
49
- stopSpinnerFail(text?: string): void {
150
+ stopThinkingFail(msg: string): void {
50
151
  if (this.spinner) {
51
- this.spinner.fail(text || this.spinner.text);
152
+ this.spinner.fail(chalk.red(msg));
52
153
  this.spinner = null;
53
154
  }
54
155
  }
55
156
 
157
+ // ── Messages ────────────────────────────────────────────
158
+
159
+ info(msg: string): void {
160
+ console.log(chalk.gray(' ') + msg);
161
+ }
162
+
163
+ success(msg: string): void {
164
+ console.log(chalk.green(' ✅ ') + msg);
165
+ }
166
+
167
+ error(msg: string): void {
168
+ console.log();
169
+ console.log(chalk.red(' Error'));
170
+ console.log(chalk.red(' ' + '─'.repeat(40)));
171
+ for (const line of msg.split('\n')) {
172
+ console.log(chalk.red(' ') + line);
173
+ }
174
+ console.log();
175
+ }
176
+
177
+ warning(msg: string): void {
178
+ console.log(chalk.yellow(' ⚠ ') + msg);
179
+ }
180
+
181
+ dim(msg: string): void {
182
+ console.log(chalk.dim(' ' + msg));
183
+ }
184
+
185
+ // ── Layout ──────────────────────────────────────────────
186
+
187
+ divider(): void {
188
+ console.log(chalk.gray(' ' + '─'.repeat(52)));
189
+ }
190
+
191
+ spacer(): void {
192
+ console.log();
193
+ }
194
+
56
195
  clear(): void {
57
196
  console.clear();
58
197
  }
59
198
 
60
- printBanner(): void {
61
- console.log(chalk.cyan(`
62
- ╔═══════════════════════════════════════╗
63
- ║ 🤖 Thatgfsj Code v0.4.0 ║
64
- ║ AI Coding Assistant ║
65
- ╚═══════════════════════════════════════╝
66
- `));
199
+ // ── Sections ────────────────────────────────────────────
200
+
201
+ section(title: string, items: Array<{ label: string; value: string }>): void {
202
+ console.log();
203
+ console.log(chalk.bold(' ' + title));
204
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
205
+ for (const item of items) {
206
+ console.log(' ' + chalk.gray(item.label.padEnd(14)) + item.value);
207
+ }
208
+ console.log();
67
209
  }
68
210
 
211
+ // ── Help ────────────────────────────────────────────────
212
+
69
213
  printHelp(): void {
70
- console.log(`
71
- ${chalk.cyan('Commands:')}
72
- ${chalk.gray('exit, Ctrl+C')} - Exit
73
- ${chalk.gray('clear')} - Clear screen
74
- ${chalk.gray('help')} - Show this help
75
- ${chalk.gray('tools')} - List available tools
76
- ${chalk.gray('model')} - Show current model
77
-
78
- ${chalk.cyan('Tips:')}
79
- • Use ${chalk.gray('↑/↓')} for command history
80
- Use ${chalk.gray('Tab')} for auto-complete
81
- `);
214
+ console.log();
215
+ console.log(chalk.bold(' Commands'));
216
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
217
+ const cmds: [string, string][] = [
218
+ ['help', 'Show this help'],
219
+ ['tools', 'List available tools'],
220
+ ['model', 'Show current model info'],
221
+ ['clear', 'Clear screen'],
222
+ ['exit', 'Exit'],
223
+ ];
224
+ for (const [cmd, desc] of cmds) {
225
+ console.log(' ' + chalk.cyan(cmd.padEnd(14)) + chalk.gray(desc));
226
+ }
227
+ console.log();
228
+ console.log(chalk.bold(' Keyboard'));
229
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
230
+ console.log(' ' + chalk.cyan('↑ / ↓'.padEnd(14)) + chalk.gray('Browse command history'));
231
+ console.log(' ' + chalk.cyan('Tab'.padEnd(14)) + chalk.gray('Auto-complete'));
232
+ console.log(' ' + chalk.cyan('Ctrl+C'.padEnd(14)) + chalk.gray('Exit'));
233
+ console.log();
82
234
  }
83
235
  }
package/src/tui/repl.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * REPL - Main interactive loop
3
- * Migrated from old src/repl/loop.ts + src/index.ts interactive mode
4
3
  */
5
4
 
6
5
  import chalk from 'chalk';
@@ -21,53 +20,45 @@ export class REPL {
21
20
  this.app = app;
22
21
  }
23
22
 
24
- /**
25
- * Start the REPL
26
- */
27
23
  async start(): Promise<void> {
28
24
  this.running = true;
29
25
 
30
- // Show banner
31
26
  this.output.clear();
32
27
  this.output.printBanner();
33
- this.output.printInfo('Type "help" for available commands\n');
34
- this.output.printDivider();
35
28
 
36
- // Show project context
37
- console.log(chalk.gray(ProjectContext.getSummary()));
38
- this.output.printDivider();
39
- console.log();
29
+ const cfg = this.app.config.get();
30
+ this.output.section('Session', [
31
+ { label: 'Provider', value: cfg.provider },
32
+ { label: 'Model', value: cfg.model },
33
+ { label: 'Project', value: ProjectContext.detectType() },
34
+ ]);
35
+
36
+ this.output.info('Type ' + chalk.cyan('help') + ' for commands');
37
+ this.output.spacer();
40
38
 
41
- // Main loop
42
39
  while (this.running) {
43
40
  try {
44
- const userInput = await this.input.prompt();
45
-
41
+ const userInput = await this.input.prompt(this.output.getPrompt());
46
42
  if (!userInput.trim()) continue;
47
43
 
48
- // Handle commands
49
44
  const handled = await this.handleCommand(userInput.trim());
50
45
  if (handled) continue;
51
46
 
52
- // Process with AI
53
47
  await this.processInput(userInput.trim());
54
48
  } catch (error: any) {
55
49
  if (error.message === 'SIGINT') continue;
56
- this.output.printError(error.message);
50
+ this.output.error(error.message);
57
51
  }
58
52
  }
59
53
  }
60
54
 
61
- /**
62
- * Handle built-in commands
63
- */
64
55
  private async handleCommand(input: string): Promise<boolean> {
65
56
  const cmd = input.toLowerCase();
66
57
 
67
58
  switch (cmd) {
68
59
  case 'exit':
69
60
  case 'quit':
70
- this.output.printInfo('\n👋 Goodbye!');
61
+ this.output.dim('Goodbye! 👋');
71
62
  this.running = false;
72
63
  this.input.close();
73
64
  return true;
@@ -82,50 +73,109 @@ export class REPL {
82
73
  return true;
83
74
 
84
75
  case 'tools':
85
- this.output.printHeader('🔧 Available Tools');
86
- for (const tool of this.app.tools.list()) {
87
- this.output.printInfo(` ${tool.name} - ${tool.description}`);
88
- }
89
- console.log();
76
+ this.output.section('Available Tools',
77
+ this.app.tools.list().map(t => ({
78
+ label: t.name,
79
+ value: t.description,
80
+ }))
81
+ );
90
82
  return true;
91
83
 
92
- case 'model':
93
- this.output.printHeader('🤖 Current Model');
94
- const cfg = this.app.config.get();
95
- this.output.printInfo(` Provider: ${cfg.provider}`);
96
- this.output.printInfo(` Model: ${cfg.model}`);
97
- this.output.printInfo(` Base URL: ${cfg.baseUrl || 'default'}`);
98
- console.log();
84
+ case 'model': {
85
+ const c = this.app.config.get();
86
+ this.output.section('Current Model', [
87
+ { label: 'Provider', value: c.provider },
88
+ { label: 'Model', value: c.model },
89
+ { label: 'Base URL', value: c.baseUrl || 'default' },
90
+ { label: 'API Key', value: c.apiKey ? '••••' + c.apiKey.slice(-4) : 'not set' },
91
+ ]);
99
92
  return true;
93
+ }
100
94
 
101
95
  default:
102
96
  return false;
103
97
  }
104
98
  }
105
99
 
106
- /**
107
- * Process user input with AI
108
- */
109
100
  private async processInput(input: string): Promise<void> {
110
101
  this.app.session.addMessage('user', input);
102
+ this.output.printUserInput(input);
103
+ this.output.startThinking();
111
104
 
112
- this.output.startSpinner('Thinking...');
113
105
  let fullResponse = '';
106
+ let hasStartedOutput = false;
114
107
 
115
108
  try {
116
- const stream = this.app.getAgent().run(this.app.session.getMessages());
109
+ const stream = this.app.streamResponse();
110
+
117
111
  for await (const chunk of stream) {
118
- this.output.stopSpinner();
119
- process.stdout.write(chunk);
112
+ this.output.stopThinking();
113
+
114
+ // Check for structured tool messages
115
+ if (chunk.includes('@@TOOL@@')) {
116
+ const parts = chunk.split('\n');
117
+ for (const part of parts) {
118
+ if (part.startsWith('@@TOOL@@')) {
119
+ try {
120
+ const data = JSON.parse(part.slice(8));
121
+ if (data.action === 'call') {
122
+ if (hasStartedOutput) {
123
+ this.output.endAssistant();
124
+ hasStartedOutput = false;
125
+ }
126
+ this.output.printToolCall(data.name, data.args || '');
127
+ this.output.startExecuting(data.name);
128
+ } else if (data.action === 'result') {
129
+ this.output.stopThinking();
130
+ this.output.printToolResult(data.output || data.error || '', !!data.error);
131
+ this.output.printToolEnd();
132
+ }
133
+ } catch {
134
+ // If parse fails, treat as regular text
135
+ if (part) {
136
+ if (!hasStartedOutput) {
137
+ this.output.beginAssistant();
138
+ hasStartedOutput = true;
139
+ }
140
+ this.output.writeChunk(part);
141
+ }
142
+ }
143
+ } else if (part) {
144
+ // Regular text between tool messages
145
+ if (!hasStartedOutput) {
146
+ this.output.beginAssistant();
147
+ hasStartedOutput = true;
148
+ }
149
+ this.output.writeChunk(part + '\n');
150
+ }
151
+ }
152
+ } else {
153
+ // Pure text chunk - just output it
154
+ if (!hasStartedOutput) {
155
+ this.output.beginAssistant();
156
+ hasStartedOutput = true;
157
+ }
158
+ this.output.writeChunk(chunk);
159
+ }
160
+
120
161
  fullResponse += chunk;
121
162
  }
122
163
 
123
- console.log(); // newline
164
+ if (hasStartedOutput) {
165
+ this.output.endAssistant();
166
+ }
167
+
168
+ if (fullResponse.trim()) {
169
+ this.app.session.addMessage('assistant', fullResponse);
170
+ this.app.session.truncate();
171
+ }
124
172
 
125
- this.app.session.addMessage('assistant', fullResponse);
126
- this.app.session.truncate();
127
173
  } catch (error: any) {
128
- this.output.stopSpinnerFail(chalk.red(`Error: ${error.message}`));
174
+ if (hasStartedOutput) {
175
+ this.output.endAssistant();
176
+ }
177
+ this.output.stopThinkingFail(error.message);
178
+ this.output.error(error.message);
129
179
  }
130
180
  }
131
181
  }