ys-code-agent 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import chalk from 'chalk';
2
2
  import { getLogger } from '../logger/index.js';
3
3
  import { configManager } from '../config/index.js';
4
- import { phoneConfig } from './phoneOptimizer.js';
5
4
  import { CommandPopup, CATEGORY_ICONS } from './CommandPopup.js';
6
5
  import { generateWelcome } from './WelcomeScreen.js';
6
+ import { renderMarkdown } from '../utils/renderMarkdown.js';
7
+ import { cursorTo, clearLine, moveCursor, emitKeypressEvents } from 'readline';
7
8
  const logger = getLogger('ui');
8
9
  const INDICATORS = {
9
10
  idle: '○',
@@ -27,24 +28,21 @@ export class TUI {
27
28
  status = 'idle';
28
29
  approvalMode = 'normal';
29
30
  agentMode = 'chat';
30
- outputLines = [];
31
- maxOutputLines = 1000;
32
31
  onInput = null;
33
- onSpecialKey = null;
32
+ onCancelRequest = null;
34
33
  running = false;
35
34
  commandPopup;
36
- inputBuffer = '';
37
35
  inputHistory = [];
38
36
  historyIndex = -1;
39
- cursorPos = 0;
40
37
  popupActive = false;
41
38
  cancelRequested = false;
42
- onCancelRequest = null;
43
- stdinRaw = false;
39
+ inputBuffer = '';
40
+ cursorPos = 0;
41
+ popupLineCount = 0;
42
+ prevInput = null;
44
43
  constructor() {
45
44
  this.commandPopup = new CommandPopup();
46
45
  this.loadHistory();
47
- this.setupRawMode();
48
46
  }
49
47
  loadHistory() {
50
48
  try {
@@ -69,340 +67,280 @@ export class TUI {
69
67
  }
70
68
  catch { }
71
69
  }
72
- setupRawMode() {
73
- if (!process.stdin.isTTY)
74
- return;
75
- try {
76
- process.stdin.setRawMode?.(true);
77
- this.stdinRaw = true;
78
- }
79
- catch {
80
- this.stdinRaw = false;
81
- }
82
- }
83
70
  setOnCancelRequest(cb) {
84
71
  this.onCancelRequest = cb;
85
72
  }
86
73
  start() {
87
74
  this.running = true;
88
- this.showPrompt();
89
- this.startKeyListener();
90
- }
91
- startKeyListener() {
92
75
  if (!process.stdin.isTTY) {
93
76
  this.startLineMode();
94
77
  return;
95
78
  }
96
- process.stdin.on('data', (data) => {
97
- if (!this.running)
98
- return;
99
- const input = data.toString('utf-8');
100
- if (this.popupActive) {
101
- this.handlePopupKey(input);
102
- this.renderScreen();
103
- return;
104
- }
105
- if (input === '\x1b' || input === '\x1b[' || input === '\x1b[' && data.length > 2) {
106
- if (input === '\x1b') {
107
- if (this.inputBuffer.length === 0) {
108
- void this.handleSpecialKey('escape');
109
- }
110
- else {
111
- this.inputBuffer = '';
112
- this.cursorPos = 0;
113
- this.renderScreen();
114
- }
115
- return;
116
- }
117
- return;
118
- }
119
- if (input === '\x1b[A') {
120
- void this.handleSpecialKey('up');
121
- return;
122
- }
123
- if (input === '\x1b[B') {
124
- void this.handleSpecialKey('down');
125
- return;
126
- }
127
- if (input === '\x1b[C') {
128
- void this.handleSpecialKey('right');
129
- return;
130
- }
131
- if (input === '\x1b[D') {
132
- void this.handleSpecialKey('left');
133
- return;
134
- }
135
- if (input === '\r' || input === '\n') {
136
- void this.handleSpecialKey('enter');
137
- return;
138
- }
139
- if (input === '\x7f' || input === '\b') {
140
- void this.handleSpecialKey('backspace');
141
- return;
79
+ this.startTTYMode();
80
+ }
81
+ startLineMode() {
82
+ const { createInterface } = require('readline');
83
+ const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '' });
84
+ rl.on('line', (line) => {
85
+ const trimmed = line.trim();
86
+ if (trimmed && this.onInput) {
87
+ this.addHistory(trimmed);
88
+ this.onInput(trimmed);
142
89
  }
143
- if (input === '\x03') {
144
- void this.handleSpecialKey('ctrl_c');
145
- return;
90
+ rl.prompt();
91
+ });
92
+ rl.on('SIGINT', () => process.exit(0));
93
+ rl.prompt();
94
+ }
95
+ startTTYMode() {
96
+ try {
97
+ process.stdin.setRawMode?.(true);
98
+ }
99
+ catch { }
100
+ emitKeypressEvents(process.stdin);
101
+ this.inputBuffer = '';
102
+ this.cursorPos = 0;
103
+ this.popupLineCount = 0;
104
+ process.stdin.on('keypress', this.handleKeypress.bind(this));
105
+ this.renderScreen();
106
+ }
107
+ async handleKeypress(str, key) {
108
+ if (!this.running)
109
+ return;
110
+ if (!key)
111
+ key = {};
112
+ const k = key.name || '';
113
+ const c = key.ctrl || false;
114
+ if (this.popupActive) {
115
+ await this.handlePopupKeypress(str, key);
116
+ return;
117
+ }
118
+ if (k === 'escape') {
119
+ if (this.inputBuffer.length > 0) {
120
+ this.inputBuffer = '';
121
+ this.cursorPos = 0;
146
122
  }
147
- if (input === '\x0c') {
148
- void this.handleSpecialKey('ctrl_l');
149
- return;
123
+ this.renderScreen();
124
+ return;
125
+ }
126
+ if (k === 'return' || k === 'enter') {
127
+ const input = this.inputBuffer.trim();
128
+ this.inputBuffer = '';
129
+ this.cursorPos = 0;
130
+ if (input) {
131
+ this.addHistory(input);
132
+ if (this.onInput) {
133
+ await this.onInput(input);
134
+ }
150
135
  }
151
- if (input === '\x01') {
152
- void this.handleSpecialKey('ctrl_a');
153
- return;
136
+ this.renderScreen();
137
+ return;
138
+ }
139
+ if (k === 'backspace') {
140
+ if (this.cursorPos > 0) {
141
+ this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos - 1) + this.inputBuffer.slice(this.cursorPos);
142
+ this.cursorPos--;
154
143
  }
155
- if (input === '\x05') {
156
- void this.handleSpecialKey('ctrl_e');
157
- return;
144
+ this.renderScreen();
145
+ return;
146
+ }
147
+ if (k === 'up') {
148
+ if (this.inputHistory.length > 0) {
149
+ if (this.historyIndex === -1) {
150
+ this.historyIndex = this.inputHistory.length - 1;
151
+ }
152
+ else if (this.historyIndex > 0) {
153
+ this.historyIndex--;
154
+ }
155
+ this.inputBuffer = this.inputHistory[this.historyIndex];
156
+ this.cursorPos = this.inputBuffer.length;
158
157
  }
159
- if (input === '\x0a' || input === '\x0d') {
160
- return;
158
+ this.renderScreen();
159
+ return;
160
+ }
161
+ if (k === 'down') {
162
+ if (this.historyIndex >= 0) {
163
+ this.historyIndex++;
164
+ if (this.historyIndex >= this.inputHistory.length) {
165
+ this.historyIndex = -1;
166
+ this.inputBuffer = '';
167
+ }
168
+ else {
169
+ this.inputBuffer = this.inputHistory[this.historyIndex];
170
+ }
171
+ this.cursorPos = this.inputBuffer.length;
161
172
  }
162
- if (input === '\x09') {
163
- void this.handleSpecialKey('tab');
164
- return;
173
+ this.renderScreen();
174
+ return;
175
+ }
176
+ if (k === 'left') {
177
+ if (this.cursorPos > 0)
178
+ this.cursorPos--;
179
+ this.renderScreen();
180
+ return;
181
+ }
182
+ if (k === 'right') {
183
+ if (this.cursorPos < this.inputBuffer.length)
184
+ this.cursorPos++;
185
+ this.renderScreen();
186
+ return;
187
+ }
188
+ if (k === 'tab') {
189
+ this.handleTab();
190
+ this.renderScreen();
191
+ return;
192
+ }
193
+ if (c && k === 'c') {
194
+ if (this.cancelRequested || this.status === 'thinking' || this.status === 'executing') {
195
+ if (this.onCancelRequest)
196
+ this.onCancelRequest();
165
197
  }
166
- if (input === '\x0f') {
167
- void this.handleSpecialKey('ctrl_o');
168
- return;
198
+ else {
199
+ this.printLine('');
200
+ this.printLine(chalk.yellow('Use /exit to quit, or press Ctrl+C again to force quit'));
201
+ this.cancelRequested = true;
202
+ setTimeout(() => { this.cancelRequested = false; }, 2000);
169
203
  }
170
- if (input === '\x18') {
171
- void this.handleSpecialKey('ctrl_x');
204
+ this.renderScreen();
205
+ return;
206
+ }
207
+ if (c && k === 'l') {
208
+ this.clear();
209
+ this.renderScreen();
210
+ return;
211
+ }
212
+ if (c && k === 'x') {
213
+ await this.openExternalEditor();
214
+ return;
215
+ }
216
+ if (c && k === 'a') {
217
+ this.cursorPos = 0;
218
+ this.renderScreen();
219
+ return;
220
+ }
221
+ if (c && k === 'e') {
222
+ this.cursorPos = this.inputBuffer.length;
223
+ this.renderScreen();
224
+ return;
225
+ }
226
+ if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
227
+ this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos) + str + this.inputBuffer.slice(this.cursorPos);
228
+ this.cursorPos++;
229
+ if (str === '/') {
230
+ this.popupActive = true;
231
+ this.commandPopup.open();
232
+ this.commandPopup.setFilter('');
233
+ this.renderScreen();
172
234
  return;
173
235
  }
174
- this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos) + input + this.inputBuffer.slice(this.cursorPos);
175
- this.cursorPos += input.length;
176
- if (input === '/') {
236
+ if (this.inputBuffer.startsWith('/') && this.inputBuffer.length > 1) {
177
237
  this.popupActive = true;
178
238
  this.commandPopup.open();
179
- this.commandPopup.setFilter(input);
180
- }
181
- else {
182
- if (this.inputBuffer.startsWith('/') && this.inputBuffer.length > 1) {
183
- this.popupActive = true;
184
- this.commandPopup.open();
185
- this.commandPopup.setFilter(this.inputBuffer);
186
- }
239
+ this.commandPopup.setFilter(this.inputBuffer.slice(1));
240
+ this.renderScreen();
241
+ return;
187
242
  }
188
- this.renderScreen();
189
- });
190
- process.stdin.on('end', () => { });
243
+ }
244
+ this.renderScreen();
191
245
  }
192
- startLineMode() {
193
- const { createInterface } = require('readline');
194
- const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '' });
195
- rl.on('line', (line) => {
196
- this.inputBuffer = line.trim();
197
- if (this.inputBuffer && this.onInput) {
198
- this.addHistory(this.inputBuffer);
199
- this.onInput(this.inputBuffer);
246
+ clearPopupLines() {
247
+ if (this.popupLineCount > 0) {
248
+ try {
249
+ moveCursor(process.stdout, 0, -(this.popupLineCount + 1));
200
250
  }
201
- this.inputBuffer = '';
202
- this.showPrompt();
203
- });
204
- rl.on('SIGINT', () => process.exit(0));
205
- this.showPrompt();
251
+ catch { }
252
+ for (let i = 0; i <= this.popupLineCount; i++) {
253
+ try {
254
+ cursorTo(process.stdout, 0);
255
+ clearLine(process.stdout, 1);
256
+ process.stdout.write('\n');
257
+ }
258
+ catch { }
259
+ }
260
+ this.popupLineCount = 0;
261
+ }
206
262
  }
207
- handlePopupKey(input) {
208
- if (input === '\x1b') {
263
+ async handlePopupKeypress(str, key) {
264
+ const k = key.name || '';
265
+ if (k === 'escape') {
266
+ this.clearPopupLines();
209
267
  this.popupActive = false;
210
268
  this.commandPopup.close();
211
269
  this.inputBuffer = '';
212
270
  this.cursorPos = 0;
271
+ this.renderScreen();
213
272
  return;
214
273
  }
215
- if (input === '\x1b[A' || input === '\x1bOA') {
274
+ if (k === 'up') {
216
275
  this.commandPopup.moveUp();
276
+ this.renderScreen();
217
277
  return;
218
278
  }
219
- if (input === '\x1b[B' || input === '\x1bOB') {
279
+ if (k === 'down') {
220
280
  this.commandPopup.moveDown();
281
+ this.renderScreen();
221
282
  return;
222
283
  }
223
- if (input === '\r' || input === '\n') {
224
- const selectedCmd = this.commandPopup.getSelectedCommand();
225
- if (selectedCmd) {
226
- this.popupActive = false;
227
- this.commandPopup.close();
228
- const fullCmd = `/${selectedCmd} `;
229
- this.inputBuffer = fullCmd;
230
- this.cursorPos = fullCmd.length;
284
+ if (k === 'return' || k === 'enter') {
285
+ const cmd = this.commandPopup.getSelectedCommand();
286
+ this.popupActive = false;
287
+ this.commandPopup.close();
288
+ this.clearPopupLines();
289
+ if (cmd) {
290
+ const fullInput = `/${cmd} `;
291
+ if (this.onInput) {
292
+ await this.onInput(fullInput.trim());
293
+ }
231
294
  }
295
+ this.renderScreen();
232
296
  return;
233
297
  }
234
- if (input === '\x09') {
235
- const selectedCmd = this.commandPopup.getSelectedCommand();
236
- if (selectedCmd) {
298
+ if (k === 'backspace') {
299
+ this.commandPopup.deleteChar();
300
+ const filter = this.commandPopup.getFilterText();
301
+ if (filter.length === 0) {
302
+ this.clearPopupLines();
237
303
  this.popupActive = false;
238
304
  this.commandPopup.close();
239
- const fullCmd = `/${selectedCmd} `;
240
- this.inputBuffer = fullCmd;
241
- this.cursorPos = fullCmd.length;
305
+ this.inputBuffer = '/';
306
+ this.cursorPos = 1;
242
307
  }
308
+ this.renderScreen();
243
309
  return;
244
310
  }
245
- if (input === '\x7f' || input === '\b') {
246
- const currentFilter = this.commandPopup.getFilterText();
247
- if (currentFilter.length <= 1) {
248
- this.popupActive = false;
249
- this.commandPopup.close();
250
- this.inputBuffer = '';
251
- this.cursorPos = 0;
252
- return;
253
- }
254
- this.commandPopup.deleteChar();
311
+ if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
312
+ this.commandPopup.appendChar(str);
313
+ this.renderScreen();
255
314
  return;
256
315
  }
257
- if (input.length === 1 && input.charCodeAt(0) >= 32) {
258
- this.commandPopup.appendChar(input);
259
- this.inputBuffer = this.commandPopup.getFilterText();
260
- this.cursorPos = this.inputBuffer.length;
261
- }
262
316
  }
263
- async handleSpecialKey(key) {
264
- switch (key) {
265
- case 'escape':
266
- if (this.inputBuffer.length > 0) {
267
- this.inputBuffer = '';
268
- this.cursorPos = 0;
269
- this.renderScreen();
270
- }
271
- break;
272
- case 'enter':
273
- if (this.inputBuffer.trim()) {
274
- this.addHistory(this.inputBuffer.trim());
275
- const msg = this.inputBuffer.trim();
276
- this.inputBuffer = '';
277
- this.cursorPos = 0;
278
- this.clearOutputLine();
279
- if (this.onInput)
280
- this.onInput(msg);
281
- }
282
- this.renderScreen();
283
- break;
284
- case 'backspace':
285
- if (this.cursorPos > 0) {
286
- this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos - 1) + this.inputBuffer.slice(this.cursorPos);
287
- this.cursorPos--;
288
- this.updatePopupFilter();
289
- }
290
- this.renderScreen();
291
- break;
292
- case 'up':
293
- if (this.inputHistory.length > 0) {
294
- if (this.historyIndex === -1) {
295
- this.historyIndex = this.inputHistory.length - 1;
296
- }
297
- else if (this.historyIndex > 0) {
298
- this.historyIndex--;
299
- }
300
- this.inputBuffer = this.inputHistory[this.historyIndex];
301
- this.cursorPos = this.inputBuffer.length;
302
- this.renderScreen();
303
- }
304
- break;
305
- case 'down':
306
- if (this.historyIndex >= 0) {
307
- this.historyIndex++;
308
- if (this.historyIndex >= this.inputHistory.length) {
309
- this.historyIndex = -1;
310
- this.inputBuffer = '';
311
- }
312
- else {
313
- this.inputBuffer = this.inputHistory[this.historyIndex];
314
- }
315
- this.cursorPos = this.inputBuffer.length;
316
- this.renderScreen();
317
- }
318
- break;
319
- case 'left':
320
- if (this.cursorPos > 0) {
321
- this.cursorPos--;
322
- this.renderScreen();
323
- }
324
- break;
325
- case 'right':
326
- if (this.cursorPos < this.inputBuffer.length) {
327
- this.cursorPos++;
328
- this.renderScreen();
329
- }
330
- break;
331
- case 'ctrl_a':
332
- this.cursorPos = 0;
333
- this.renderScreen();
334
- break;
335
- case 'ctrl_e':
317
+ handleTab() {
318
+ const match = this.inputBuffer.match(/^\/?(\w*)$/);
319
+ if (match) {
320
+ const partial = match[1].toLowerCase();
321
+ const { ALL_COMMANDS } = require('./CommandPopup.js');
322
+ const cmds = ALL_COMMANDS.map((c) => c.command).filter((c) => c.startsWith(partial));
323
+ if (cmds.length === 1) {
324
+ this.inputBuffer = `/${cmds[0]} `;
336
325
  this.cursorPos = this.inputBuffer.length;
337
- this.renderScreen();
338
- break;
339
- case 'ctrl_c':
340
- if (this.cancelRequested || this.status === 'thinking' || this.status === 'executing') {
341
- if (this.onCancelRequest)
342
- this.onCancelRequest();
343
- }
344
- else {
345
- this.printLine('\n' + chalk.yellow('Use /exit to quit, or press Ctrl+C again to force quit'));
346
- this.cancelRequested = true;
347
- setTimeout(() => { this.cancelRequested = false; }, 2000);
348
- }
349
- break;
350
- case 'ctrl_l':
351
- this.clear();
352
- this.printWelcome();
353
- this.showPrompt();
354
- break;
355
- case 'tab': {
356
- const match = this.inputBuffer.match(/^\/?(\w*)$/);
357
- if (match) {
358
- const partial = match[1].toLowerCase();
359
- const cmds = this.getAllCommands().filter((c) => c.startsWith(partial));
360
- if (cmds.length === 1) {
361
- this.inputBuffer = `/${cmds[0]} `;
362
- this.cursorPos = this.inputBuffer.length;
363
- this.renderScreen();
364
- }
365
- }
366
- break;
367
- }
368
- case 'ctrl_o':
369
- break;
370
- case 'ctrl_x':
371
- void this.openExternalEditor();
372
- break;
373
- }
374
- }
375
- updatePopupFilter() {
376
- if (this.popupActive) {
377
- this.commandPopup.setFilter(this.inputBuffer);
378
- if (this.inputBuffer === '/' || this.inputBuffer === '') {
379
- this.popupActive = false;
380
- this.commandPopup.close();
381
326
  }
382
327
  }
383
328
  }
384
- getAllCommands() {
385
- const { ALL_COMMANDS } = require('./CommandPopup.js');
386
- return ALL_COMMANDS.map((c) => c.command);
387
- }
388
329
  async openExternalEditor() {
389
330
  const editor = process.env.EDITOR || 'nano';
390
- const { writeFileSync, unlinkSync, existsSync, mkdirSync } = await import('fs');
391
- const { join } = await import('path');
331
+ const { writeFileSync, unlinkSync, existsSync, mkdirSync, readFileSync } = await import('fs');
392
332
  const tmpDir = '/tmp/ys-agent';
393
333
  if (!existsSync(tmpDir))
394
334
  mkdirSync(tmpDir, { recursive: true });
395
- const tmpFile = join(tmpDir, `input-${Date.now()}.md`);
335
+ const tmpFile = `/tmp/ys-agent/input-${Date.now()}.md`;
396
336
  writeFileSync(tmpFile, this.inputBuffer, 'utf-8');
397
337
  const { execSync } = await import('child_process');
398
338
  try {
399
339
  execSync(`${editor} "${tmpFile}"`, { stdio: 'inherit' });
400
- const { readFileSync } = await import('fs');
401
340
  const content = readFileSync(tmpFile, 'utf-8').trim();
402
341
  if (content) {
403
342
  this.inputBuffer = content;
404
343
  this.cursorPos = content.length;
405
- this.renderScreen();
406
344
  }
407
345
  }
408
346
  catch { }
@@ -410,147 +348,42 @@ export class TUI {
410
348
  unlinkSync(tmpFile);
411
349
  }
412
350
  catch { }
413
- }
414
- addHistory(input) {
415
- if (this.inputHistory[this.inputHistory.length - 1] !== input) {
416
- this.inputHistory.push(input);
417
- }
418
- this.historyIndex = -1;
419
- this.saveHistory();
351
+ this.renderScreen();
420
352
  }
421
353
  renderScreen() {
422
354
  if (!process.stdout.isTTY)
423
355
  return;
424
- const lines = this.buildDisplayLines();
425
- const clearSeq = `\x1b[0J\x1b[${process.stdout.rows || 24};0H`;
426
- const output = clearSeq + lines.join('\n');
427
- try {
428
- cursorToSync(process.stdout, 0, (process.stdout.rows || 24) - lines.length);
429
- process.stdout.write(output);
430
- }
431
- catch { }
432
- }
433
- buildDisplayLines() {
434
- const lines = [];
435
- if (this.popupActive) {
436
- const popupLines = this.commandPopup.render().split('\n');
437
- lines.push(...popupLines);
438
- }
439
- lines.push(this.buildPromptLine());
440
- return lines;
441
- }
442
- stop() {
443
- this.running = false;
444
- if (this.stdinRaw && process.stdin.isTTY) {
356
+ const promptLine = this.buildPromptLine();
357
+ if (this.popupLineCount > 0) {
445
358
  try {
446
- process.stdin.setRawMode?.(false);
359
+ moveCursor(process.stdout, 0, -(this.popupLineCount + 1));
447
360
  }
448
361
  catch { }
449
362
  }
450
- process.stdin.removeAllListeners('data');
451
- this.showShutdownScreen();
452
- }
453
- clear() {
454
- console.clear();
455
- }
456
- clearOutputLine() {
363
+ if (this.popupActive && this.commandPopup.isVisible()) {
364
+ const popup = this.commandPopup.render();
365
+ const popupLines = popup.split('\n');
366
+ this.popupLineCount = popupLines.length;
367
+ for (const l of popupLines) {
368
+ try {
369
+ cursorTo(process.stdout, 0);
370
+ clearLine(process.stdout, 1);
371
+ process.stdout.write(l + '\n');
372
+ }
373
+ catch { }
374
+ }
375
+ }
376
+ else {
377
+ this.popupLineCount = 0;
378
+ }
457
379
  try {
458
- const { cursorTo, clearLine } = require('readline');
459
380
  cursorTo(process.stdout, 0);
460
381
  clearLine(process.stdout, 1);
382
+ process.stdout.write(promptLine);
461
383
  }
462
384
  catch { }
463
385
  }
464
- printWelcome() {
465
- const welcome = generateWelcome();
466
- const lines = welcome.split('\n');
467
- for (const line of lines) {
468
- this.printLine(line);
469
- }
470
- this.printLine('');
471
- if (configManager.getConfig().security.readOnlyMode) {
472
- this.printWarning('Read-only mode active. File modifications will be blocked.');
473
- }
474
- if (this.approvalMode === 'yolo') {
475
- this.printWarning('YOLO mode active. Agent will execute actions without confirmation.');
476
- }
477
- }
478
- printHelp() {
479
- const { ALL_COMMANDS } = require('./CommandPopup.js');
480
- const w = Math.max(Math.min(process.stdout.columns || 80, 72), 30);
481
- const top = `╔${'═'.repeat(w)}╗`;
482
- const bottom = `╚${'═'.repeat(w)}╝`;
483
- const lines = [top];
484
- const title = ` YS Code Agent — Commands `;
485
- const titlePad = Math.max(0, Math.floor((w - title.length) / 2));
486
- lines.push(`║${' '.repeat(titlePad)}${chalk.cyan(title)}${' '.repeat(Math.max(0, w - titlePad - title.length))}║`);
487
- lines.push(`╠${'═'.repeat(w)}╣`);
488
- const categories = {};
489
- for (const cmd of ALL_COMMANDS) {
490
- if (!categories[cmd.category])
491
- categories[cmd.category] = [];
492
- categories[cmd.category].push(cmd);
493
- }
494
- let first = true;
495
- for (const [cat, cmds] of Object.entries(categories)) {
496
- if (!first)
497
- lines.push(`║${' '.repeat(w)}║`);
498
- first = false;
499
- const icon = CATEGORY_ICONS[cat] || ' ';
500
- lines.push(`║ ${icon} ${chalk.white(cat.toUpperCase())}${' '.repeat(Math.max(0, w - 5 - cat.length))}║`);
501
- for (const cmd of cmds) {
502
- const usage = cmd.usage || `/${cmd.command}`;
503
- const cmdStr = chalk.yellow(usage);
504
- const desc = chalk.gray(cmd.description);
505
- const padding = ' '.repeat(Math.max(0, w - usage.length - cmd.description.length - 6));
506
- lines.push(`║ ${cmdStr}${padding}${desc} ║`);
507
- }
508
- }
509
- lines.push(`╠${'═'.repeat(w)}╣`);
510
- lines.push(`║ ${chalk.gray('Keyboard Shortcuts:')}${' '.repeat(Math.max(0, w - 22))}║`);
511
- const shortcuts = [
512
- ['↑/↓', 'History navigation'],
513
- ['Tab', 'Autocomplete commands'],
514
- ['Ctrl+C', 'Cancel request'],
515
- ['Ctrl+L', 'Clear screen'],
516
- ['Ctrl+A', 'Line start'],
517
- ['Ctrl+E', 'Line end'],
518
- ['Ctrl+X', 'Open editor'],
519
- ['Esc', 'Cancel / clear input'],
520
- ];
521
- for (const [key, desc] of shortcuts) {
522
- const keyStr = chalk.cyan(key);
523
- const descStr = chalk.gray(desc);
524
- const padding = ' '.repeat(Math.max(0, w - key.length - desc.length - 6));
525
- lines.push(`║ ${keyStr}${padding}${descStr} ║`);
526
- }
527
- lines.push(bottom);
528
- for (const l of lines)
529
- this.printLine(l);
530
- }
531
- printWarning(message) {
532
- const w = Math.max(Math.min(process.stdout.columns || 80, 60), 10);
533
- const top = `╔${'═'.repeat(w)}╗`;
534
- const bottom = `╚${'═'.repeat(w)}╝`;
535
- this.printLine('');
536
- this.printLine(chalk.yellow(top));
537
- const warnLine = ` ⚠ ${chalk.yellow(message)}`;
538
- const pad = Math.max(0, w - warnLine.length);
539
- this.printLine(`║${warnLine}${' '.repeat(pad)}║`);
540
- this.printLine(chalk.yellow(bottom));
541
- }
542
- showPrompt() {
543
- if (!this.running)
544
- return;
545
- const promptLine = this.buildPromptLine();
546
- if (phoneConfig.isTermux) {
547
- this.printLine(promptLine);
548
- }
549
- else {
550
- this.renderScreen();
551
- }
552
- }
553
- buildPromptLine() {
386
+ buildPromptPrefix() {
554
387
  const statusIndicator = STATUS_COLORS[this.status](INDICATORS[this.status]);
555
388
  const config = configManager.getConfig();
556
389
  const model = config.model.model;
@@ -567,28 +400,37 @@ export class TUI {
567
400
  approvalBadge = chalk.cyan(' [safe]');
568
401
  else if (this.approvalMode === 'yolo')
569
402
  approvalBadge = chalk.red(' [yolo]');
570
- const promptStr = `${statusIndicator} ${chalk.cyan('ys')} ${chalk.yellow(`[${modelShort}]`)}${modeBadge}${approvalBadge} ${chalk.gray('›')} `;
571
- const inputStr = this.inputBuffer || ' ';
572
- const cursorChar = phoneConfig.isTermux ? '█' : '█';
573
- const cursor = chalk.gray(cursorChar);
574
- const displayedInput = inputStr.length > 0 ? inputStr : '';
575
- let cursorLine = '';
576
- if (this.cursorPos >= displayedInput.length) {
577
- cursorLine = promptStr + displayedInput + cursor;
403
+ return `${statusIndicator} ${chalk.cyan('ys')} ${chalk.yellow(`[${modelShort}]`)}${modeBadge}${approvalBadge} ${chalk.gray('›')} `;
404
+ }
405
+ buildPromptLine() {
406
+ const prefix = this.buildPromptPrefix();
407
+ const input = this.inputBuffer || '';
408
+ const cursor = chalk.gray('');
409
+ let line;
410
+ if (this.cursorPos >= input.length) {
411
+ line = prefix + input + cursor;
578
412
  }
579
413
  else {
580
- const before = displayedInput.slice(0, this.cursorPos);
581
- const after = displayedInput.slice(this.cursorPos);
582
- cursorLine = promptStr + before + cursor + after;
414
+ const before = input.slice(0, this.cursorPos);
415
+ const after = input.slice(this.cursorPos);
416
+ line = prefix + before + cursor + after;
417
+ }
418
+ return line;
419
+ }
420
+ showPrompt() {
421
+ if (this.running)
422
+ this.renderScreen();
423
+ }
424
+ addHistory(input) {
425
+ if (this.inputHistory[this.inputHistory.length - 1] !== input) {
426
+ this.inputHistory.push(input);
583
427
  }
584
- return cursorLine + '\n' + chalk.gray(` 💡 / for commands | ↑↓ history | Tab complete`);
428
+ this.historyIndex = -1;
429
+ this.saveHistory();
585
430
  }
586
431
  setOnInput(handler) {
587
432
  this.onInput = handler;
588
433
  }
589
- setOnSpecialKey(handler) {
590
- this.onSpecialKey = handler;
591
- }
592
434
  setStatus(status) {
593
435
  this.status = status;
594
436
  }
@@ -610,32 +452,28 @@ export class TUI {
610
452
  return;
611
453
  }
612
454
  try {
613
- const { cursorTo, clearLine } = require('readline');
614
455
  cursorTo(process.stdout, 0);
615
456
  clearLine(process.stdout, 1);
616
- console.log(line);
457
+ process.stdout.write(line + '\n');
617
458
  }
618
459
  catch {
619
460
  console.log(line);
620
461
  }
621
- this.outputLines.push(line);
622
- if (this.outputLines.length > this.maxOutputLines) {
623
- this.outputLines.shift();
624
- }
625
462
  }
626
463
  printAssistant(message) {
627
- this.printLine('');
628
- const formatted = chalk.green(message);
629
- this.printLine(formatted);
464
+ const rendered = renderMarkdown(message);
465
+ for (const line of rendered.split('\n')) {
466
+ this.printLine(line);
467
+ }
468
+ }
469
+ printWarning(message) {
470
+ this.printLine(chalk.yellow(`⚠ ${message}`));
630
471
  }
631
472
  printError(error) {
632
473
  const w = Math.max(Math.min(process.stdout.columns || 80, 56), 10);
633
- const top = `╔${'═'.repeat(w)}╗`;
634
- const bottom = `╚${''.repeat(w)}╝`;
635
- this.printLine('');
636
- this.printLine(chalk.red(top));
637
- this.printLine(`║ ${chalk.red('✗')} ${chalk.white(error)}${' '.repeat(Math.max(0, w - error.length - 7))}║`);
638
- this.printLine(chalk.red(bottom));
474
+ this.printLine(chalk.red(`╔${'═'.repeat(w)}╗`));
475
+ this.printLine(chalk.red(`║`) + ` ${chalk.red('✗')} ${chalk.white(error)}${' '.repeat(Math.max(0, w - error.length - 7))}` + chalk.red(`║`));
476
+ this.printLine(chalk.red(`╚${''.repeat(w)}╝`));
639
477
  }
640
478
  printToolCall(toolName, args) {
641
479
  const argsStr = Object.entries(args)
@@ -652,75 +490,91 @@ export class TUI {
652
490
  this.printLine(chalk.gray(` ✕ ${chalk.red(result.error || 'failed')}`));
653
491
  }
654
492
  }
655
- showStatusBar(info) {
656
- const statusColor = STATUS_COLORS[info.status];
657
- const statusIcon = INDICATORS[info.status];
658
- const truncated = info.task ? info.task.slice(0, 40) + (info.task.length > 40 ? '...' : '') : '';
659
- const parts = [
660
- `${statusColor(`${statusIcon} ${info.status}`)}`,
661
- chalk.gray(`msgs:${info.messages}`),
662
- chalk.gray(`tok:${info.tokens}`),
663
- ];
664
- if (info.provider)
665
- parts.push(chalk.gray(`prov:${info.provider}`));
666
- if (truncated)
667
- parts.push(chalk.gray(`task:${truncated}`));
668
- }
669
- showShutdownScreen() {
670
- const w = Math.max(Math.min(process.stdout.columns || 80, 50), 30);
671
- const top = `╭${'─'.repeat(w)}╮`;
672
- const bottom = `╰${'─'.repeat(w)}╯`;
673
- console.log('');
674
- console.log(chalk.cyan(top));
675
- console.log(`│${' '.repeat(w)}│`);
676
- const titleLine = ' ◆ YS Code Agent — Session Complete ';
677
- const titlePad = Math.max(0, Math.floor((w - titleLine.length) / 2));
678
- console.log(`│${' '.repeat(titlePad)}${chalk.cyan(titleLine)}${' '.repeat(Math.max(0, w - titlePad - titleLine.length))}│`);
679
- console.log(`│${' '.repeat(w)}│`);
680
- const { sessionManager } = require('../session/index.js');
681
- const session = sessionManager.getCurrentSession();
682
- if (session) {
683
- const sessionId = session.id.slice(0, 8);
684
- const duration = formatDuration(Date.now() - session.createdAt);
685
- const msgCount = session.state.messages.length;
686
- console.log(`│ ${chalk.white('Session')}: ${chalk.yellow(sessionId.padEnd(w - 13))}│`);
687
- console.log(`│ ${chalk.white('Duration')}: ${chalk.yellow(duration.padEnd(w - 14))}│`);
688
- console.log(`│ ${chalk.white('Messages')}: ${chalk.yellow(String(msgCount).padEnd(w - 14))}│`);
689
- }
690
- console.log(`│${' '.repeat(w)}│`);
691
- console.log(`│ ${chalk.gray('Session saved. Resume with:')} ${chalk.yellow('ys --resume <session>')}${' '.repeat(Math.max(0, w - 43))}│`);
692
- console.log(`│${' '.repeat(w)}│`);
693
- console.log(chalk.cyan(bottom));
694
- console.log('');
493
+ showStatusBar(_info) {
695
494
  }
696
- destroy() {
697
- if (this.stdinRaw && process.stdin.isTTY) {
698
- try {
699
- process.stdin.setRawMode?.(false);
495
+ printWelcome() {
496
+ const welcome = generateWelcome();
497
+ for (const line of welcome.split('\n')) {
498
+ this.printLine(line);
499
+ }
500
+ this.printLine('');
501
+ if (configManager.getConfig().security.readOnlyMode) {
502
+ this.printWarning('Read-only mode active. File modifications will be blocked.');
503
+ }
504
+ if (this.approvalMode === 'yolo') {
505
+ this.printWarning('YOLO mode active. Agent will execute actions without confirmation.');
506
+ }
507
+ }
508
+ printHelp() {
509
+ const { ALL_COMMANDS } = require('./CommandPopup.js');
510
+ const w = Math.max(Math.min(process.stdout.columns || 80, 72), 30);
511
+ const top = `╔${'═'.repeat(w)}╗`;
512
+ const bottom = `╚${'═'.repeat(w)}╝`;
513
+ const lines = [top];
514
+ const title = ` YS Code Agent — Commands `;
515
+ const titlePad = Math.max(0, Math.floor((w - title.length) / 2));
516
+ lines.push(`║${' '.repeat(titlePad)}${chalk.cyan(title)}${' '.repeat(Math.max(0, w - titlePad - title.length))}║`);
517
+ lines.push(`╠${'═'.repeat(w)}╣`);
518
+ const categories = {};
519
+ for (const cmd of ALL_COMMANDS) {
520
+ if (!categories[cmd.category])
521
+ categories[cmd.category] = [];
522
+ categories[cmd.category].push(cmd);
523
+ }
524
+ let first = true;
525
+ for (const [cat, cmds] of Object.entries(categories)) {
526
+ if (!first)
527
+ lines.push(`║${' '.repeat(w)}║`);
528
+ first = false;
529
+ const icon = CATEGORY_ICONS[cat] || ' ';
530
+ lines.push(`║ ${icon} ${chalk.white(cat.toUpperCase())}${' '.repeat(Math.max(0, w - 5 - cat.length))}║`);
531
+ for (const cmd of cmds) {
532
+ const usage = cmd.usage || `/${cmd.command}`;
533
+ const cmdStr = chalk.yellow(usage);
534
+ const desc = chalk.gray(cmd.description);
535
+ const padding = ' '.repeat(Math.max(0, w - usage.length - cmd.description.length - 6));
536
+ lines.push(`║ ${cmdStr}${padding}${desc} ║`);
700
537
  }
701
- catch { }
702
538
  }
703
- process.stdin.removeAllListeners('data');
539
+ lines.push(`╠${''.repeat(w)}╣`);
540
+ lines.push(`║ ${chalk.gray('Keyboard Shortcuts:')}${' '.repeat(Math.max(0, w - 22))}║`);
541
+ const shortcuts = [
542
+ ['↑/↓', 'History navigation'],
543
+ ['Tab', 'Autocomplete commands'],
544
+ ['Ctrl+C', 'Cancel request'],
545
+ ['Ctrl+L', 'Clear screen'],
546
+ ['Ctrl+A', 'Line start'],
547
+ ['Ctrl+E', 'Line end'],
548
+ ['Ctrl+X', 'Open editor'],
549
+ ['Esc', 'Cancel / clear input'],
550
+ ];
551
+ for (const [key, desc] of shortcuts) {
552
+ const keyStr = chalk.cyan(key);
553
+ const descStr = chalk.gray(desc);
554
+ const padding = ' '.repeat(Math.max(0, w - key.length - desc.length - 6));
555
+ lines.push(`║ ${keyStr}${padding}${descStr} ║`);
556
+ }
557
+ lines.push(bottom);
558
+ for (const l of lines)
559
+ this.printLine(l);
704
560
  }
705
- getOutputLineCount() {
706
- return this.outputLines.length;
561
+ clear() {
562
+ console.clear();
707
563
  }
708
- }
709
- function formatDuration(ms) {
710
- if (ms < 1000)
711
- return `${ms}ms`;
712
- if (ms < 60000)
713
- return `${(ms / 1000).toFixed(1)}s`;
714
- const m = Math.floor(ms / 60000);
715
- const s = Math.floor((ms % 60000) / 1000);
716
- return `${m}m ${s}s`;
717
- }
718
- function cursorToSync(stream, x, y) {
719
- try {
720
- const { cursorTo } = require('readline');
721
- cursorTo(stream, x, y);
564
+ stop() {
565
+ this.running = false;
566
+ process.stdin.removeAllListeners('keypress');
567
+ try {
568
+ process.stdin.setRawMode?.(false);
569
+ }
570
+ catch { }
571
+ }
572
+ destroy() {
573
+ this.stop();
574
+ }
575
+ getOutputLineCount() {
576
+ return 0;
722
577
  }
723
- catch { }
724
578
  }
725
579
  export const tui = new TUI();
726
580
  //# sourceMappingURL=index.js.map