agent-termo 1.0.0 → 1.0.2
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/README.md +30 -11
- package/dist/agents/index.d.ts +5 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +86 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +97 -164
- package/dist/cli.js.map +4 -4
- package/dist/diff/index.d.ts +9 -0
- package/dist/diff/index.d.ts.map +1 -0
- package/dist/diff/index.js +213 -0
- package/dist/diff/index.js.map +1 -0
- package/dist/llm/client.d.ts +39 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +188 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/models.d.ts +19 -0
- package/dist/llm/models.d.ts.map +1 -0
- package/dist/llm/models.js +45 -0
- package/dist/llm/models.js.map +1 -0
- package/dist/llm/tools.d.ts +3 -0
- package/dist/llm/tools.d.ts.map +1 -0
- package/dist/llm/tools.js +204 -0
- package/dist/llm/tools.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +91 -140
- package/dist/main.js.map +4 -4
- package/dist/memory/index.d.ts +10 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +117 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +334 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/index.d.ts +119 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/index.d.ts +47 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +321 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/screen.d.ts +59 -0
- package/dist/ui/screen.d.ts.map +1 -0
- package/dist/ui/screen.js +629 -0
- package/dist/ui/screen.js.map +1 -0
- package/dist/utils/ansi.d.ts +62 -0
- package/dist/utils/ansi.d.ts.map +1 -0
- package/dist/utils/ansi.js +116 -0
- package/dist/utils/ansi.js.map +1 -0
- package/dist/utils/config.d.ts +9 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +120 -0
- package/dist/utils/config.js.map +1 -0
- package/package.json +2 -2
- package/src/cli.ts +91 -12
- package/src/llm/client.ts +24 -0
- package/src/llm/models.ts +55 -0
- package/src/main.ts +201 -75
- package/src/ui/index.ts +256 -389
- package/src/ui/screen.ts +698 -0
- package/src/utils/config.ts +62 -2
package/src/ui/index.ts
CHANGED
|
@@ -1,58 +1,175 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
// Terminal UI — chalk-based,
|
|
2
|
+
// Terminal UI — chalk-based styling, markdown output, diff viewer
|
|
3
|
+
// Layout (pinned input box, scroll region) and keyboard input live in screen.ts
|
|
3
4
|
// =============================================================================
|
|
4
5
|
|
|
5
6
|
import Chalk from 'chalk';
|
|
6
7
|
import type { ProposedFileChange } from '../types/index.js';
|
|
7
8
|
import { loadAgentTermoMemory, loadAgentTermoTimeline } from '../memory/index.js';
|
|
8
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
clipText,
|
|
11
|
+
listenKeys,
|
|
12
|
+
resetScreen,
|
|
13
|
+
setCommands,
|
|
14
|
+
terminalWidth,
|
|
15
|
+
type CommandInfo,
|
|
16
|
+
} from './screen.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
createInput,
|
|
20
|
+
initScreen,
|
|
21
|
+
pick,
|
|
22
|
+
setScreenStatus,
|
|
23
|
+
type InputController,
|
|
24
|
+
type PickItem,
|
|
25
|
+
} from './screen.js';
|
|
9
26
|
|
|
10
27
|
const c: typeof Chalk = Chalk as unknown as typeof Chalk;
|
|
28
|
+
const out = process.stdout;
|
|
29
|
+
|
|
30
|
+
export const COMMANDS: CommandInfo[] = [
|
|
31
|
+
{ name: '/help', description: 'show all commands' },
|
|
32
|
+
{ name: '/clear', description: 'clear conversation & screen' },
|
|
33
|
+
{ name: '/memory', description: 'view Agent-Termo memory' },
|
|
34
|
+
{ name: '/timeline', description: 'view recent activity' },
|
|
35
|
+
{ name: '/model', description: 'switch NVIDIA NIM model (or /model <id>)' },
|
|
36
|
+
{ name: '/agents', description: 'list all agents' },
|
|
37
|
+
{ name: '/history', description: 'show message count' },
|
|
38
|
+
{ name: '/exit', description: 'quit' },
|
|
39
|
+
];
|
|
40
|
+
setCommands(COMMANDS);
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line no-control-regex
|
|
43
|
+
const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
44
|
+
const visibleLength = (s: string): number => s.replace(ANSI_RE, '').length;
|
|
45
|
+
const rule = (): string => c.dim('─'.repeat(Math.max(10, Math.min(terminalWidth() - 2, 100))));
|
|
11
46
|
|
|
12
47
|
// =============================================================================
|
|
13
48
|
// WELCOME BANNER
|
|
14
49
|
// =============================================================================
|
|
15
50
|
|
|
16
|
-
export function printWelcome(projectRoot: string): void {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
export function printWelcome(projectRoot: string, model: string): void {
|
|
52
|
+
const width = Math.max(40, Math.min(terminalWidth() - 2, 64));
|
|
53
|
+
const inner = width - 2;
|
|
54
|
+
const room = inner - 2;
|
|
55
|
+
const row = (content: string): string =>
|
|
56
|
+
' ' + c.magenta('│') + ' ' + content + ' '.repeat(Math.max(0, room - visibleLength(content))) + ' ' + c.magenta('│');
|
|
57
|
+
const field = (label: string, value: string): string =>
|
|
58
|
+
c.dim(label.padEnd(7)) + c.cyan(clipText(value, room - 7));
|
|
59
|
+
|
|
60
|
+
out.write('\n');
|
|
61
|
+
out.write(' ' + c.magenta('╭' + '─'.repeat(inner) + '╮') + '\n');
|
|
62
|
+
out.write(row(c.magenta.bold('✻ Agent-Termo')) + '\n');
|
|
63
|
+
out.write(row(c.dim(clipText('AI coding agent for your terminal', room))) + '\n');
|
|
64
|
+
out.write(row('') + '\n');
|
|
65
|
+
out.write(row(field('model', model)) + '\n');
|
|
66
|
+
out.write(row(field('cwd', projectRoot)) + '\n');
|
|
67
|
+
out.write(' ' + c.magenta('╰' + '─'.repeat(inner) + '╯') + '\n');
|
|
68
|
+
out.write('\n');
|
|
69
|
+
out.write(
|
|
70
|
+
c.dim(' ') + c.cyan('/help') + c.dim(' commands · ') + c.cyan('/model') + c.dim(' switch NIM model · ') +
|
|
71
|
+
c.cyan('Tab') + c.dim(' completes') + '\n\n',
|
|
34
72
|
);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// MARKDOWN OUTPUT — line-buffered so streamed replies render as they arrive
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
function inline(text: string): string {
|
|
80
|
+
return text
|
|
81
|
+
.split(/(`[^`]+`)/)
|
|
82
|
+
.map((seg) =>
|
|
83
|
+
seg.length > 1 && seg.startsWith('`') && seg.endsWith('`')
|
|
84
|
+
? c.cyan(seg.slice(1, -1))
|
|
85
|
+
: seg.replace(/\*\*(.+?)\*\*/g, (_m, g: string) => c.bold(g)),
|
|
86
|
+
)
|
|
87
|
+
.join('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class MarkdownPrinter {
|
|
91
|
+
private pending = '';
|
|
92
|
+
private inCode = false;
|
|
93
|
+
private first = true;
|
|
94
|
+
private lastBlank = false;
|
|
95
|
+
|
|
96
|
+
/** `beforeFirstOutput` runs once, just before the first line is printed (stop the spinner there). */
|
|
97
|
+
constructor(private readonly beforeFirstOutput?: () => void) {}
|
|
98
|
+
|
|
99
|
+
push(text: string): void {
|
|
100
|
+
this.pending += text;
|
|
101
|
+
let nl = this.pending.indexOf('\n');
|
|
102
|
+
while (nl !== -1) {
|
|
103
|
+
const line = this.pending.slice(0, nl).replace(/\r$/, '');
|
|
104
|
+
this.pending = this.pending.slice(nl + 1);
|
|
105
|
+
this.emit(line);
|
|
106
|
+
nl = this.pending.indexOf('\n');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
end(): void {
|
|
111
|
+
if (this.pending) this.emit(this.pending.replace(/\r$/, ''));
|
|
112
|
+
this.pending = '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get printed(): boolean {
|
|
116
|
+
return !this.first;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private emit(line: string): void {
|
|
120
|
+
const blank = line.trim() === '';
|
|
121
|
+
if (!this.inCode && blank && (this.first || this.lastBlank)) return;
|
|
122
|
+
this.lastBlank = blank && !this.inCode;
|
|
123
|
+
|
|
124
|
+
if (this.first) {
|
|
125
|
+
this.beforeFirstOutput?.();
|
|
126
|
+
out.write('\n');
|
|
127
|
+
}
|
|
128
|
+
const prefix = this.first ? c.magenta('● ') : ' ';
|
|
129
|
+
this.first = false;
|
|
130
|
+
// Blank prose lines stay empty (no trailing indent); code keeps its gutter.
|
|
131
|
+
out.write(blank && !this.inCode ? '\n' : prefix + this.render(line) + '\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private render(line: string): string {
|
|
135
|
+
const fence = /^\s*```\s*([\w+-]*)/.exec(line);
|
|
136
|
+
if (fence) {
|
|
137
|
+
this.inCode = !this.inCode;
|
|
138
|
+
return this.inCode ? c.dim('┌─ ' + (fence[1] || 'code')) : c.dim('└─');
|
|
139
|
+
}
|
|
140
|
+
if (this.inCode) return c.dim('│ ') + c.cyan(line.replace(/\t/g, ' '));
|
|
141
|
+
|
|
142
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
143
|
+
if (heading) return c.bold.magenta(inline(heading[1] ?? ''));
|
|
144
|
+
if (/^\s*([-*_])\1{2,}\s*$/.test(line)) return c.dim('─'.repeat(Math.min(40, terminalWidth() - 4)));
|
|
145
|
+
|
|
146
|
+
const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
|
|
147
|
+
if (bullet) return (bullet[1] ?? '') + c.magenta('•') + ' ' + inline(bullet[2] ?? '');
|
|
148
|
+
|
|
149
|
+
const numbered = /^(\s*)(\d+)[.)]\s+(.*)$/.exec(line);
|
|
150
|
+
if (numbered) return (numbered[1] ?? '') + c.magenta(numbered[2] + '.') + ' ' + inline(numbered[3] ?? '');
|
|
151
|
+
|
|
152
|
+
const quote = /^>\s?(.*)$/.exec(line);
|
|
153
|
+
if (quote) return c.dim('▎ ') + c.italic(inline(quote[1] ?? ''));
|
|
154
|
+
|
|
155
|
+
return inline(line);
|
|
156
|
+
}
|
|
38
157
|
}
|
|
39
158
|
|
|
40
159
|
// =============================================================================
|
|
41
160
|
// MESSAGE DISPLAY
|
|
42
161
|
// =============================================================================
|
|
43
162
|
|
|
44
|
-
export function printUserMessage(
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log(c.green(msg.content) + '\n');
|
|
163
|
+
export function printUserMessage(text: string): void {
|
|
164
|
+
const lines = text.split('\n');
|
|
165
|
+
out.write('\n' + c.green.bold('❯ ') + c.bold(lines[0] ?? '') + '\n');
|
|
166
|
+
for (const l of lines.slice(1)) out.write(' ' + c.bold(l) + '\n');
|
|
49
167
|
}
|
|
50
168
|
|
|
51
169
|
export function printAssistantMessage(content: string): void {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
console.log(c.magenta(content) + '\n');
|
|
170
|
+
const md = new MarkdownPrinter();
|
|
171
|
+
md.push(content);
|
|
172
|
+
md.end();
|
|
56
173
|
}
|
|
57
174
|
|
|
58
175
|
// =============================================================================
|
|
@@ -60,13 +177,14 @@ export function printAssistantMessage(content: string): void {
|
|
|
60
177
|
// =============================================================================
|
|
61
178
|
|
|
62
179
|
export function printToolCall(toolName: string, args: Record<string, unknown>): void {
|
|
63
|
-
const
|
|
64
|
-
.map(([k, v]) =>
|
|
180
|
+
const summary = Object.entries(args)
|
|
181
|
+
.map(([k, v]) => {
|
|
182
|
+
const raw = typeof v === 'string' ? v : JSON.stringify(v);
|
|
183
|
+
return k + '=' + clipText(String(raw ?? '').replace(/\s+/g, ' '), 40);
|
|
184
|
+
})
|
|
65
185
|
.join(', ');
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const truncated = argsSummary.slice(0, 200);
|
|
69
|
-
if (truncated) console.log(c.dim(' ' + truncated));
|
|
186
|
+
const room = terminalWidth() - toolName.length - 6;
|
|
187
|
+
out.write('\n' + c.blue('● ') + c.bold(toolName) + c.dim('(' + clipText(summary, room) + ')') + '\n');
|
|
70
188
|
}
|
|
71
189
|
|
|
72
190
|
export function printToolResult(
|
|
@@ -74,122 +192,81 @@ export function printToolResult(
|
|
|
74
192
|
result: { success: boolean; output?: string; error?: string },
|
|
75
193
|
parsedOutput?: { needsApproval?: boolean; proposedChange?: ProposedFileChange },
|
|
76
194
|
): void {
|
|
77
|
-
//
|
|
195
|
+
// A proposed file change is shown as a diff, then the accept/reject prompt.
|
|
78
196
|
if (parsedOutput?.needsApproval && parsedOutput.proposedChange) {
|
|
79
|
-
console.log('');
|
|
80
197
|
printDiffViewer(parsedOutput.proposedChange);
|
|
81
198
|
return;
|
|
82
199
|
}
|
|
83
200
|
|
|
201
|
+
const room = terminalWidth() - 8;
|
|
84
202
|
if (result.success) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const short = out.length > 80 ? out.slice(0, 80) + '...' : out;
|
|
91
|
-
console.log(c.green(' ' + short));
|
|
203
|
+
// One line of output is shown as-is; longer output (file contents, command
|
|
204
|
+
// logs) is summarised, since the full text goes to the model.
|
|
205
|
+
const lines = (result.output ?? '').split('\n').filter((l) => l.trim() !== '');
|
|
206
|
+
const summary = lines.length > 1 ? `${lines.length} lines` : clipText((lines[0] ?? '').trim() || 'done', room);
|
|
207
|
+
out.write(c.dim(' ⎿ ') + c.green('✓ ') + summary + '\n');
|
|
92
208
|
} else {
|
|
93
|
-
|
|
94
|
-
|
|
209
|
+
const message = (result.error ?? 'unknown error').split('\n')[0] ?? 'unknown error';
|
|
210
|
+
out.write(c.dim(' ⎿ ') + c.red('✗ ' + clipText(`${toolName}: ${message}`, room)) + '\n');
|
|
95
211
|
}
|
|
96
212
|
}
|
|
97
213
|
|
|
98
214
|
// =============================================================================
|
|
99
|
-
// DIFF VIEWER
|
|
215
|
+
// DIFF VIEWER
|
|
100
216
|
// =============================================================================
|
|
101
217
|
|
|
102
218
|
export function printDiffViewer(change: ProposedFileChange): void {
|
|
103
219
|
const diff = change.diff;
|
|
220
|
+
const room = Math.max(20, terminalWidth() - 14);
|
|
104
221
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
console.log(c.bold(c.cyan(' ─── ' + diff.file + ' ───')));
|
|
108
|
-
|
|
109
|
-
// Stat line
|
|
110
|
-
const actionLabel = change.isDeletion
|
|
111
|
-
? c.red('[- DELETE]')
|
|
222
|
+
const label = change.isDeletion
|
|
223
|
+
? c.red('DELETE')
|
|
112
224
|
: change.isNewFile
|
|
113
|
-
? c.green('
|
|
114
|
-
: c.blue('
|
|
115
|
-
|
|
116
|
-
|
|
225
|
+
? c.green('NEW')
|
|
226
|
+
: c.blue('MODIFY');
|
|
227
|
+
|
|
228
|
+
out.write('\n ' + c.bold.cyan(diff.file) + ' ' + label + ' ' + c.green('+' + diff.additions) + ' ' + c.red('-' + diff.deletions) + '\n');
|
|
229
|
+
out.write(' ' + c.dim('─'.repeat(Math.max(10, Math.min(terminalWidth() - 4, 100)))) + '\n');
|
|
117
230
|
|
|
118
|
-
// Diff lines
|
|
119
231
|
for (const line of diff.lines) {
|
|
120
232
|
const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' ';
|
|
121
233
|
const color = line.type === 'add' ? c.green : line.type === 'remove' ? c.red : c.dim;
|
|
122
|
-
const
|
|
123
|
-
|
|
234
|
+
const num = line.lineNumber > 0 ? String(line.lineNumber).padStart(4) : ' ';
|
|
235
|
+
const text = clipText(line.content.replace(/\t/g, ' '), room);
|
|
236
|
+
out.write(' ' + color(prefix + num + ' │ ' + text) + '\n');
|
|
124
237
|
}
|
|
125
|
-
|
|
126
|
-
// Summary footer
|
|
127
|
-
console.log('');
|
|
128
|
-
console.log(c.dim(' ── ') + c.green('+' + diff.additions) + c.dim(' added, ') + c.red('-' + diff.deletions) + c.dim(' removed ') + c.dim('─'));
|
|
238
|
+
out.write('\n');
|
|
129
239
|
}
|
|
130
240
|
|
|
131
241
|
// =============================================================================
|
|
132
|
-
// ACCEPT / REJECT PROMPT
|
|
242
|
+
// ACCEPT / REJECT PROMPT
|
|
133
243
|
// =============================================================================
|
|
134
244
|
|
|
135
|
-
export
|
|
136
|
-
|
|
137
|
-
|
|
245
|
+
export function promptAcceptReject(message: string = 'Accept this change?'): Promise<'accept' | 'reject'> {
|
|
246
|
+
out.write(
|
|
247
|
+
'\n' + c.yellow('? ') + c.bold(message) + c.dim(' [') + c.green('Y') + c.dim('/') + c.red('n') + c.dim('] '),
|
|
248
|
+
);
|
|
138
249
|
|
|
139
250
|
return new Promise((resolve) => {
|
|
140
|
-
|
|
141
|
-
let
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
case 'Y':
|
|
151
|
-
case 'enter':
|
|
152
|
-
case 'return':
|
|
153
|
-
process.stdout.write(c.green('yes') + '\n');
|
|
154
|
-
cleanup();
|
|
155
|
-
resolve('accept');
|
|
156
|
-
break;
|
|
157
|
-
case 'n':
|
|
158
|
-
case 'N':
|
|
159
|
-
case 'escape':
|
|
160
|
-
process.stdout.write(c.red('no') + '\n');
|
|
161
|
-
cleanup();
|
|
162
|
-
resolve('reject');
|
|
163
|
-
break;
|
|
164
|
-
case 'ctrl+c':
|
|
165
|
-
process.stdout.write(c.red('cancelled') + '\n');
|
|
166
|
-
cleanup();
|
|
167
|
-
resolve('reject');
|
|
168
|
-
break;
|
|
169
|
-
default:
|
|
170
|
-
// Ignore other keys, re-arm for next key
|
|
171
|
-
alreadyHandled = false;
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const cleanup = (): void => {
|
|
177
|
-
process.stdin.setRawMode(false);
|
|
178
|
-
process.stdin.pause();
|
|
179
|
-
process.stdin.removeListener('data', onData);
|
|
251
|
+
let done = false;
|
|
252
|
+
let stop: () => void = () => undefined;
|
|
253
|
+
|
|
254
|
+
const answer = (accepted: boolean | 'cancelled'): void => {
|
|
255
|
+
done = true;
|
|
256
|
+
stop();
|
|
257
|
+
out.write(
|
|
258
|
+
accepted === true ? c.green('yes') + '\n' : accepted === false ? c.red('no') + '\n' : c.red('cancelled') + '\n',
|
|
259
|
+
);
|
|
260
|
+
resolve(accepted === true ? 'accept' : 'reject');
|
|
180
261
|
};
|
|
181
262
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
process.stdin.setRawMode(true);
|
|
190
|
-
process.stdin.resume();
|
|
191
|
-
process.stdin.setEncoding('utf8');
|
|
192
|
-
process.stdin.on('data', onData);
|
|
263
|
+
stop = listenKeys((key) => {
|
|
264
|
+
if (done) return;
|
|
265
|
+
const first = key.text?.[0]?.toLowerCase();
|
|
266
|
+
if (key.name === 'enter' || first === 'y') answer(true);
|
|
267
|
+
else if (key.name === 'escape' || first === 'n') answer(false);
|
|
268
|
+
else if (key.name === 'ctrl+c') answer('cancelled');
|
|
269
|
+
});
|
|
193
270
|
});
|
|
194
271
|
}
|
|
195
272
|
|
|
@@ -198,65 +275,45 @@ export async function promptAcceptReject(message: string = 'Accept this change?'
|
|
|
198
275
|
// =============================================================================
|
|
199
276
|
|
|
200
277
|
export function printError(message: string, hint?: string): void {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (hint) console.log(c.dim(' Hint: ' + hint));
|
|
278
|
+
out.write('\n' + c.red('✗ ' + message) + '\n');
|
|
279
|
+
if (hint) out.write(c.dim(' Hint: ' + hint) + '\n');
|
|
204
280
|
}
|
|
205
281
|
|
|
206
282
|
export function printSuccess(message: string): void {
|
|
207
|
-
|
|
283
|
+
out.write(c.green('✓ ' + message) + '\n');
|
|
208
284
|
}
|
|
209
285
|
|
|
210
286
|
export function printInfo(message: string): void {
|
|
211
|
-
|
|
287
|
+
out.write(c.blue('ℹ ') + message + '\n');
|
|
212
288
|
}
|
|
213
289
|
|
|
214
290
|
// =============================================================================
|
|
215
291
|
// AGENT-TERMO MEMORY + TIMELINE DISPLAY
|
|
216
292
|
// =============================================================================
|
|
217
293
|
|
|
218
|
-
function clearCurrentLine(): void {
|
|
219
|
-
process.stdout.write('\x1b[2K');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
294
|
export async function printMemoryFile(): Promise<void> {
|
|
223
295
|
const mem = loadAgentTermoMemory();
|
|
224
296
|
if (!mem) {
|
|
225
|
-
|
|
226
|
-
clearCurrentLine();
|
|
227
|
-
process.stdout.write(c.blue('[Info] ') + 'Agent-Termo memory is empty. (Normal for new sessions.)\n');
|
|
297
|
+
printInfo('Agent-Termo memory is empty. (Normal for new sessions.)');
|
|
228
298
|
return;
|
|
229
299
|
}
|
|
230
|
-
|
|
231
|
-
clearCurrentLine();
|
|
232
|
-
process.stdout.write(c.bold('=== AGENT-TERMO MEMORY ===') + '\n');
|
|
233
|
-
process.stdout.write(c.dim('─'.repeat(80)) + '\n');
|
|
234
|
-
process.stdout.write(mem + '\n');
|
|
235
|
-
process.stdout.write(c.dim('─'.repeat(80)) + '\n\n');
|
|
300
|
+
out.write('\n' + c.bold('Agent-Termo memory') + '\n' + rule() + '\n' + mem + '\n' + rule() + '\n\n');
|
|
236
301
|
}
|
|
237
302
|
|
|
238
303
|
export async function printTimelineFile(): Promise<void> {
|
|
239
304
|
const tl = loadAgentTermoTimeline();
|
|
240
305
|
if (!tl || tl.trim().length === 0) {
|
|
241
|
-
|
|
242
|
-
clearCurrentLine();
|
|
243
|
-
process.stdout.write(c.blue('[Info] ') + 'Timeline is empty.\n');
|
|
306
|
+
printInfo('Timeline is empty.');
|
|
244
307
|
return;
|
|
245
308
|
}
|
|
246
309
|
const lines = tl.split('\n');
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
process.stdout.write(c.bold('=== AGENT-TERMO TIMELINE (recent entries) ===') + '\n');
|
|
253
|
-
process.stdout.write(c.dim('─'.repeat(80)) + '\n');
|
|
254
|
-
process.stdout.write(recent + '\n');
|
|
255
|
-
process.stdout.write(c.dim('─'.repeat(80)) + '\n');
|
|
256
|
-
if (totalLines > recentCount) {
|
|
257
|
-
process.stdout.write(c.dim('... (' + (totalLines - recentCount) + ' earlier entries hidden)') + '\n');
|
|
310
|
+
const recentCount = Math.min(40, lines.length);
|
|
311
|
+
out.write('\n' + c.bold('Agent-Termo timeline') + c.dim(' (recent entries)') + '\n' + rule() + '\n');
|
|
312
|
+
out.write(lines.slice(-recentCount).join('\n') + '\n' + rule() + '\n');
|
|
313
|
+
if (lines.length > recentCount) {
|
|
314
|
+
out.write(c.dim('… ' + (lines.length - recentCount) + ' earlier entries hidden') + '\n');
|
|
258
315
|
}
|
|
259
|
-
|
|
316
|
+
out.write('\n');
|
|
260
317
|
}
|
|
261
318
|
|
|
262
319
|
// =============================================================================
|
|
@@ -264,256 +321,66 @@ export async function printTimelineFile(): Promise<void> {
|
|
|
264
321
|
// =============================================================================
|
|
265
322
|
|
|
266
323
|
export function printHelp(): void {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
324
|
+
out.write('\n' + c.bold('Commands') + '\n' + rule() + '\n');
|
|
325
|
+
for (const cmd of COMMANDS) {
|
|
326
|
+
out.write(' ' + c.cyan(cmd.name.padEnd(11)) + c.dim(cmd.description) + '\n');
|
|
327
|
+
}
|
|
328
|
+
out.write('\n' + c.bold('Keys') + '\n' + rule() + '\n');
|
|
329
|
+
const keys: Array<[string, string]> = [
|
|
330
|
+
['↑ / ↓', 'input history'],
|
|
331
|
+
['Tab', 'complete a /command'],
|
|
332
|
+
['Ctrl+A / Ctrl+E', 'start / end of line'],
|
|
333
|
+
['Ctrl+W / Ctrl+U', 'delete word / to line start'],
|
|
334
|
+
['Ctrl+C', 'clear the line, or quit when empty'],
|
|
335
|
+
];
|
|
336
|
+
for (const [k, d] of keys) out.write(' ' + c.cyan(k.padEnd(17)) + c.dim(d) + '\n');
|
|
337
|
+
out.write('\n');
|
|
278
338
|
}
|
|
279
339
|
|
|
280
340
|
export function printAgentsList(agents: Array<{ name: string; description: string }>): void {
|
|
281
|
-
|
|
282
|
-
|
|
341
|
+
out.write('\n' + c.bold('Available agents') + '\n' + rule() + '\n');
|
|
342
|
+
const room = Math.max(10, terminalWidth() - 34);
|
|
283
343
|
for (const a of agents) {
|
|
284
|
-
|
|
285
|
-
process.stdout.write(' ' + c.cyan(a.name.padEnd(28)) + ' ' + c.dim(short) + '\n');
|
|
344
|
+
out.write(' ' + c.cyan(a.name.padEnd(30)) + c.dim(clipText(a.description.replace(/\s+/g, ' '), room)) + '\n');
|
|
286
345
|
}
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// =============================================================================
|
|
291
|
-
// INPUT STATE + REPL LOOP
|
|
292
|
-
// =============================================================================
|
|
293
|
-
|
|
294
|
-
class InputState {
|
|
295
|
-
line = '';
|
|
296
|
-
cursor = 0;
|
|
297
|
-
histIdx = -1;
|
|
298
|
-
history: string[] = [];
|
|
299
|
-
boundOnData: ((key: string) => void) | null = null;
|
|
300
|
-
|
|
301
|
-
constructor() { this.history = []; }
|
|
346
|
+
out.write('\n');
|
|
302
347
|
}
|
|
303
348
|
|
|
304
349
|
// =============================================================================
|
|
305
|
-
//
|
|
350
|
+
// SPINNER
|
|
306
351
|
// =============================================================================
|
|
307
352
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export function restoreCursorPosition(): void {
|
|
313
|
-
process.stdout.write('\x1b[u');
|
|
314
|
-
}
|
|
353
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
354
|
+
let spinnerTimer: ReturnType<typeof setInterval> | null = null;
|
|
315
355
|
|
|
316
|
-
export function
|
|
317
|
-
|
|
318
|
-
|
|
356
|
+
export function startSpinner(message: string): void {
|
|
357
|
+
stopSpinner();
|
|
358
|
+
if (!out.isTTY) {
|
|
359
|
+
out.write(c.dim(message + '…') + '\n');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
319
362
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
question: (prompt: string) => Promise<string>;
|
|
327
|
-
close: () => void;
|
|
328
|
-
showPrompt: () => void;
|
|
329
|
-
} {
|
|
330
|
-
let closeFn: (() => void) | null = null;
|
|
331
|
-
|
|
332
|
-
return {
|
|
333
|
-
async question(_prompt: string): Promise<string> {
|
|
334
|
-
return new Promise<string>((resolve) => {
|
|
335
|
-
process.stdin.setRawMode(true);
|
|
336
|
-
process.stdin.resume();
|
|
337
|
-
process.stdin.setEncoding('utf8');
|
|
338
|
-
process.stdin.setMaxListeners(20);
|
|
339
|
-
|
|
340
|
-
const state = new InputState();
|
|
341
|
-
const inputPrompt = c.magenta('> ');
|
|
342
|
-
|
|
343
|
-
function finalize(l: string): void {
|
|
344
|
-
if (l.trim()) {
|
|
345
|
-
state.history.unshift(l);
|
|
346
|
-
if (state.history.length > 50) state.history.pop();
|
|
347
|
-
}
|
|
348
|
-
state.histIdx = -1;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
closeFn = (): void => {
|
|
352
|
-
process.stdin.setRawMode(false);
|
|
353
|
-
process.stdin.pause();
|
|
354
|
-
finalize('');
|
|
355
|
-
if (state.boundOnData) {
|
|
356
|
-
process.stdin.removeListener('data', state.boundOnData);
|
|
357
|
-
state.boundOnData = null;
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
moveToFreshLine();
|
|
362
|
-
process.stdout.write(inputPrompt);
|
|
363
|
-
saveCursorPosition();
|
|
364
|
-
process.stdout.write('\x1b[?25h');
|
|
365
|
-
|
|
366
|
-
function onData(key: string): void {
|
|
367
|
-
const keyName = getKeyName(key);
|
|
368
|
-
|
|
369
|
-
if (keyName === 'return' || keyName === 'enter') {
|
|
370
|
-
finalize(state.line);
|
|
371
|
-
process.stdout.write('\n');
|
|
372
|
-
// CRITICAL: Remove listener so promptAcceptReject doesn't get double-handled
|
|
373
|
-
process.stdin.removeListener('data', state.boundOnData!);
|
|
374
|
-
state.boundOnData = null;
|
|
375
|
-
process.stdin.setRawMode(false);
|
|
376
|
-
process.stdin.pause();
|
|
377
|
-
resolve(state.line);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (keyName === 'ctrl+c') {
|
|
382
|
-
finalize(state.line);
|
|
383
|
-
process.stdout.write('\n');
|
|
384
|
-
process.stdin.removeListener('data', state.boundOnData!);
|
|
385
|
-
state.boundOnData = null;
|
|
386
|
-
process.stdin.setRawMode(false);
|
|
387
|
-
process.stdin.pause();
|
|
388
|
-
process.exit(0);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (keyName === 'up') {
|
|
393
|
-
if (state.history.length > 0 && state.histIdx < state.history.length - 1) {
|
|
394
|
-
state.histIdx++;
|
|
395
|
-
state.line = state.history[state.histIdx] ?? '';
|
|
396
|
-
state.cursor = state.line.length;
|
|
397
|
-
restoreCursorPosition();
|
|
398
|
-
process.stdout.write(' '.repeat(200) + '\r' + inputPrompt + state.line);
|
|
399
|
-
}
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (keyName === 'down') {
|
|
404
|
-
if (state.histIdx > 0) {
|
|
405
|
-
state.histIdx--;
|
|
406
|
-
state.line = state.history[state.histIdx] ?? '';
|
|
407
|
-
} else {
|
|
408
|
-
state.histIdx = -1;
|
|
409
|
-
state.line = '';
|
|
410
|
-
}
|
|
411
|
-
state.cursor = state.line.length;
|
|
412
|
-
restoreCursorPosition();
|
|
413
|
-
process.stdout.write(' '.repeat(200) + '\r' + inputPrompt + state.line);
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (keyName === 'backspace') {
|
|
418
|
-
if (state.cursor > 0) {
|
|
419
|
-
state.line = state.line.slice(0, state.cursor - 1) + state.line.slice(state.cursor);
|
|
420
|
-
state.cursor = Math.max(0, state.cursor - 1);
|
|
421
|
-
restoreCursorPosition();
|
|
422
|
-
process.stdout.write(' '.repeat(200) + '\r' + inputPrompt + state.line);
|
|
423
|
-
}
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (keyName === 'left') {
|
|
428
|
-
if (state.cursor > 0) {
|
|
429
|
-
state.cursor--;
|
|
430
|
-
restoreCursorPosition();
|
|
431
|
-
process.stdout.write('\r' + inputPrompt + state.line);
|
|
432
|
-
}
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (keyName === 'right') {
|
|
437
|
-
if (state.cursor < state.line.length) {
|
|
438
|
-
state.cursor++;
|
|
439
|
-
restoreCursorPosition();
|
|
440
|
-
process.stdout.write('\r' + inputPrompt + state.line);
|
|
441
|
-
}
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (key.length === 1 && key.charCodeAt(0) >= 0x20) {
|
|
446
|
-
state.line = state.line.slice(0, state.cursor) + key + state.line.slice(state.cursor);
|
|
447
|
-
state.cursor++;
|
|
448
|
-
restoreCursorPosition();
|
|
449
|
-
process.stdout.write(' '.repeat(200) + '\r' + inputPrompt + state.line);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
state.boundOnData = (key: string) => onData(key);
|
|
454
|
-
process.stdin.on('data', state.boundOnData);
|
|
455
|
-
});
|
|
456
|
-
},
|
|
457
|
-
|
|
458
|
-
close(): void { if (closeFn) closeFn(); },
|
|
459
|
-
|
|
460
|
-
showPrompt(): void {
|
|
461
|
-
moveToFreshLine();
|
|
462
|
-
process.stdout.write(c.magenta('> '));
|
|
463
|
-
saveCursorPosition();
|
|
464
|
-
},
|
|
363
|
+
const startedAt = Date.now();
|
|
364
|
+
let frame = 0;
|
|
365
|
+
const label = clipText(message, Math.max(10, terminalWidth() - 14));
|
|
366
|
+
const render = (): void => {
|
|
367
|
+
const secs = Math.floor((Date.now() - startedAt) / 1000);
|
|
368
|
+
out.write('\r\x1b[2K' + c.magenta(FRAMES[frame++ % FRAMES.length] ?? '⠋') + ' ' + c.dim(`${label}… ${secs}s`));
|
|
465
369
|
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// =============================================================================
|
|
469
|
-
// SPINNER
|
|
470
|
-
// =============================================================================
|
|
471
|
-
|
|
472
|
-
let spinnerInterval: ReturnType<typeof setInterval> | null = null;
|
|
473
370
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}, 80);
|
|
371
|
+
out.write('\x1b[?25l');
|
|
372
|
+
render();
|
|
373
|
+
spinnerTimer = setInterval(render, 80);
|
|
478
374
|
}
|
|
479
375
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
process.stdout.write('\r' + c.green('done.') + '\n');
|
|
487
|
-
} else if (success === false) {
|
|
488
|
-
process.stdout.write('\r' + c.red('failed.') + '\n');
|
|
489
|
-
}
|
|
376
|
+
/** Stops the spinner and clears its line. The argument is kept for older call sites and is ignored. */
|
|
377
|
+
export function stopSpinner(_success?: boolean): void {
|
|
378
|
+
if (!spinnerTimer) return;
|
|
379
|
+
clearInterval(spinnerTimer);
|
|
380
|
+
spinnerTimer = null;
|
|
381
|
+
out.write('\r\x1b[2K\x1b[?25h');
|
|
490
382
|
}
|
|
491
383
|
|
|
492
384
|
export function clearScreen(): void {
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// =============================================================================
|
|
497
|
-
// KEY NAME PARSER
|
|
498
|
-
// =============================================================================
|
|
499
|
-
|
|
500
|
-
export function getKeyName(ch: string): string {
|
|
501
|
-
if (ch === '\r') return 'return';
|
|
502
|
-
if (ch === '\n') return 'enter';
|
|
503
|
-
if (ch === '\x03') return 'ctrl+c';
|
|
504
|
-
if (ch === '\x1b') return 'escape';
|
|
505
|
-
if (ch === '\x7f' || ch === '\b') return 'backspace';
|
|
506
|
-
if (ch === '\x04') return 'ctrl+d';
|
|
507
|
-
if (ch === '\t') return 'tab';
|
|
508
|
-
if (ch === '\x1b[A') return 'up';
|
|
509
|
-
if (ch === '\x1b[B') return 'down';
|
|
510
|
-
if (ch === '\x1b[C') return 'right';
|
|
511
|
-
if (ch === '\x1b[D') return 'left';
|
|
512
|
-
if (ch === '\x1b[3~') return 'delete';
|
|
513
|
-
if (ch === '\x1b[F') return 'end';
|
|
514
|
-
if (ch === '\x1b[H') return 'home';
|
|
515
|
-
if (ch === '\x1b[5~') return 'pageup';
|
|
516
|
-
if (ch === '\x1b[6~') return 'pagedown';
|
|
517
|
-
if (ch.startsWith('\x1b[')) return ch;
|
|
518
|
-
return ch.length === 1 ? ch : ch;
|
|
385
|
+
resetScreen();
|
|
519
386
|
}
|