kiku-agent 1.0.5 → 1.0.6

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.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import readline from 'node:readline';
4
4
  import { stdin as input } from 'node:process';
@@ -14,18 +14,30 @@ import { eventBus, Events } from './eventBus.js';
14
14
  import { AgentStates } from './agentState.js';
15
15
  import { sanitizeQuery } from './sanitizer.js';
16
16
  import { isAbortError } from './errors.js';
17
- import { COMMAND_DEFINITIONS, SUB_COMMANDS, findCommandDefinition } from './commandRegistry.js';
17
+ import { COMMAND_DEFINITIONS, SUB_COMMANDS, findCommandDefinition, getCommandDetails } from './commandRegistry.js';
18
18
  import { ModelManager } from './modelManager.js';
19
+ import { PACKAGE_VERSION } from './version.js';
20
+ import { getTheme, getThemeName } from './theme.js';
21
+ import {
22
+ classifyToolResult,
23
+ formatDetailDuration,
24
+ parseDetailsRequest,
25
+ serializeDetailValue,
26
+ summarizeQueryDetails,
27
+ copyToClipboard,
28
+ getCopyableContent,
29
+ } from './queryDetails.js';
19
30
 
20
31
  import {
21
- C, getStringDisplayWidth, getFullWidth, wrapText, stripAnsi, fitStyledText,
32
+ C, getStringDisplayWidth, getFullWidth, wrapText, stripAnsi, fitStyledText, sanitizeTerminalText,
22
33
  previousGraphemeBoundary, nextGraphemeBoundary, sliceLineForDisplay,
23
34
  printSolidBannerRow, printSolidPanel, formatSolidPanelLines, formatTerminalMarkdown,
24
35
  parseMarkdownTableRow, isMarkdownTableSeparator, expandMarkdownLinks,
36
+ formatRichCardLines, printRichCard, printRichStatusCard, formatStatusBadge, formatRichTable,
25
37
  printWelcomeHero, getWelcomeHeroLines, randomFanName, setupOutputInterception,
26
38
  rawWrite, INPUT_PLACEHOLDER, UI_ICONS,
27
39
  spinnerFrames, thinkingFrames, createProgressRunner,
28
- enterAlternateScreen, exitAlternateScreen, VirtualScreenBuffer,
40
+ enterAlternateScreen, exitAlternateScreen, getTuiViewportLayout, VirtualScreenBuffer,
29
41
  } from './ui.js';
30
42
 
31
43
  let activeAbortController = null;
@@ -53,12 +65,14 @@ function parseCliArgs() {
53
65
  showStats: false,
54
66
  thinking: 'show',
55
67
  verbose: false,
56
- help: false
68
+ help: false,
69
+ version: false
57
70
  };
58
71
 
59
72
  for (let i = 0; i < args.length; i++) {
60
73
  const arg = args[i];
61
74
  if (arg === '--help' || arg === '-h') options.help = true;
75
+ else if (arg === '--version' || arg === '-V') options.version = true;
62
76
  else if (arg === '--tools' || arg === '-t') options.showTools = true;
63
77
  else if (arg === '--stats' || arg === '-s') options.showStats = true;
64
78
  else if (arg === '--verbose' || arg === '-v') { options.verbose = true; options.thinking = 'show'; }
@@ -91,10 +105,10 @@ async function attemptAutoStartLMStudio(baseUrl, model) {
91
105
 
92
106
  const candidates = isWin
93
107
  ? [
94
- `${process.env.LOCALAPPDATA}\\LM Studio\\LM Studio.exe`,
95
- `${process.env.LOCALAPPDATA}\\Programs\\LM Studio\\LM Studio.exe`,
96
- 'C:\\Program Files\\LM Studio\\LM Studio.exe'
97
- ]
108
+ `${process.env.LOCALAPPDATA}\\LM Studio\\LM Studio.exe`,
109
+ `${process.env.LOCALAPPDATA}\\Programs\\LM Studio\\LM Studio.exe`,
110
+ 'C:\\Program Files\\LM Studio\\LM Studio.exe'
111
+ ]
98
112
  : isMac
99
113
  ? ['/Applications/LM Studio.app/Contents/MacOS/LM Studio']
100
114
  : ['lmstudio', '/usr/local/bin/lmstudio', '/opt/lmstudio/bin/lmstudio'];
@@ -107,11 +121,15 @@ async function attemptAutoStartLMStudio(baseUrl, model) {
107
121
  });
108
122
 
109
123
  if (!exe) return null;
110
- console.log(` ${C.cyberCyan}attempting to auto-start LM Studio...${C.reset}`);
111
124
 
112
125
  try {
113
126
  let port = '1234';
114
- try { const parsedUrl = new URL(baseUrl); if (parsedUrl.port) port = parsedUrl.port; } catch {}
127
+ try { const parsedUrl = new URL(baseUrl); if (parsedUrl.port) port = parsedUrl.port; } catch { }
128
+ printRichCard({
129
+ title: 'LM Studio Auto-Start',
130
+ status: 'autostart',
131
+ lines: [`Launching local LM Studio process on port ${port}...`],
132
+ });
115
133
 
116
134
  const child = spawn(exe, isWin
117
135
  ? ['--model', model, '--serve', '--port', port]
@@ -125,12 +143,11 @@ async function attemptAutoStartLMStudio(baseUrl, model) {
125
143
  }
126
144
 
127
145
  function extractPreview(result, toolName) {
128
- const MAX = 80;
146
+ if (!result || typeof result !== 'object') return '';
129
147
  let raw = '';
130
148
  if (result.content) raw = result.content;
131
149
  else if (result.results && Array.isArray(result.results) && result.results.length > 0) {
132
- const first = result.results[0];
133
- raw = first.title || first.snippet || first.url || '';
150
+ raw = result.results.map(r => r.title || r.name || r.id || JSON.stringify(r)).join(', ');
134
151
  } else if (result.works) raw = result.works.map(w => w.titleTh || w.titleCn).filter(Boolean).join(', ');
135
152
  else if (result.songs) raw = result.songs.map(s => s.title).filter(Boolean).join(', ');
136
153
  else if (result.brands) raw = result.brands.map(b => b.brand).filter(Boolean).join(', ');
@@ -138,8 +155,7 @@ function extractPreview(result, toolName) {
138
155
  else if (result.stats) raw = `${result.stats.totalWorks || 0} works`;
139
156
 
140
157
  if (!raw) return '';
141
- raw = raw.replace(/\s+/g, ' ').trim();
142
- return raw.length > MAX ? raw.slice(0, MAX) + '…' : raw;
158
+ return raw.replace(/\s+/g, ' ').trim();
143
159
  }
144
160
 
145
161
  function cleanThinkingLine(line) {
@@ -153,77 +169,263 @@ function cleanThinkingLine(line) {
153
169
  }
154
170
 
155
171
  function printQueryDetails(reference = '') {
156
- const normalized = String(reference || '').trim().replace(/^q/i, '');
157
- const requestedNumber = normalized ? Number.parseInt(normalized, 10) : lastQueryDetails?.number;
158
- const details = normalized
159
- ? (Number.isInteger(requestedNumber) ? queryDetails.get(requestedNumber) : null)
160
- : lastQueryDetails;
172
+ const request = parseDetailsRequest(reference);
173
+ if (request.error) {
174
+ printSolidPanel({
175
+ title: 'Query Details — Invalid Request',
176
+ lines: [
177
+ `${C.amber}${request.error}${C.reset}`,
178
+ '',
179
+ `${C.white}/details [Qn]${C.reset} ${C.dim}— overview${C.reset}`,
180
+ `${C.white}/details [Qn] answer${C.reset}`,
181
+ `${C.white}/details [Qn] thinking${C.reset}`,
182
+ `${C.white}/details [Qn] tools${C.reset}`,
183
+ `${C.white}/details [Qn] tool <n>${C.reset}`,
184
+ `${C.white}/details [Qn] all${C.reset}`,
185
+ ],
186
+ });
187
+ return;
188
+ }
189
+
190
+ const details = request.queryNumber == null
191
+ ? lastQueryDetails
192
+ : queryDetails.get(request.queryNumber);
161
193
 
162
194
  if (!details) {
163
- console.log(`\n ${C.amber}[NOT FOUND] No completed query details. Use ${C.cyberCyan}/details Q1${C.reset}${C.amber}.${C.reset}\n`);
195
+ const target = request.queryNumber == null ? 'the latest query' : `Q${request.queryNumber}`;
196
+ console.log(`\n ${C.amber}[NOT FOUND] No diagnostics recorded for ${target}.${C.reset}\n`);
164
197
  return;
165
198
  }
166
199
 
167
200
  const width = Math.max(16, getFullWidth() - 8);
168
- const safeLines = (label, value) => {
169
- const text = `${label}: ${String(value || '—')}`;
170
- return wrapText(expandMarkdownLinks(text), width)
171
- .map(line => `${C.white}${formatTerminalMarkdown(line)}${C.reset}`);
201
+ const safeWrappedLines = (value, color = C.white) => {
202
+ const inputLines = String(value || '—').split('\n');
203
+ return inputLines.flatMap(line => wrapText(sanitizeTerminalText(line, 500000) || ' ', width))
204
+ .map(line => `${color}${line}${C.reset}`);
205
+ };
206
+ const summary = summarizeQueryDetails(details);
207
+ const queryLabel = `Q${details.number}`;
208
+ const statusColor = details.status === 'complete' ? C.emerald : details.status === 'aborted' ? C.amber : C.red;
209
+ const statusLabel = String(details.status || 'complete').toUpperCase();
210
+ const startedLabel = Number.isFinite(details.startedAt)
211
+ ? new Date(details.startedAt).toLocaleString()
212
+ : 'n/a';
213
+
214
+ const toolTimelineLines = (includeArgs = false) => {
215
+ if (!details.tools?.length) return [`${C.dim}${C.slate}No tools were used.${C.reset}`];
216
+ return details.tools.flatMap((tool, index) => {
217
+ const resultState = classifyToolResult(tool.result);
218
+ const stateColor = resultState.state === 'ok' ? C.emerald : resultState.state === 'not_found' ? C.amber : C.red;
219
+ const countLabel = resultState.count == null ? '' : ` • ${resultState.count} results`;
220
+ const preview = extractPreview(tool.result || {}, tool.name);
221
+ const rows = [
222
+ `${C.slate}${String(index + 1).padStart(2)}.${C.reset} ${C.bold}${C.cyberCyan}${sanitizeTerminalText(tool.name, 128)}${C.reset} ${stateColor}[${resultState.state.toUpperCase()}]${C.reset} ${C.dim}${formatDetailDuration(tool.duration)}${countLabel}${C.reset}`,
223
+ ];
224
+ if (includeArgs) {
225
+ const serializedArgs = serializeDetailValue(tool.args, Infinity).text.replace(/\s+/g, ' ');
226
+ rows.push(...safeWrappedLines(` Args: ${serializedArgs}`, C.slate));
227
+ }
228
+ if (preview) rows.push(...safeWrappedLines(` Result: ${preview}`, C.slate));
229
+ return rows;
230
+ });
172
231
  };
173
232
 
174
- printSolidPanel({
175
- title: `Details • Q${details.number}`,
176
- lines: safeLines('Question', details.prompt),
177
- });
233
+ if (request.view === 'answer') {
234
+ printSolidPanel({
235
+ title: `Answer • ${queryLabel}`,
236
+ lines: details.answer
237
+ ? String(details.answer).split('\n').flatMap(line => wrapText(sanitizeTerminalText(line, 500000), width)).map(line => `${C.white}${formatTerminalMarkdown(line)}${C.reset}`)
238
+ : [`${C.dim}${C.slate}No final answer was recorded.${C.reset}`],
239
+ });
240
+ return;
241
+ }
178
242
 
179
- const thinking = String(details.thinking || '').trim();
180
- printSolidPanel({
181
- title: `Thinking • Q${details.number}`,
182
- lines: thinking
183
- ? thinking.split('\n').flatMap(line => wrapText(cleanThinkingLine(line), width)).map(line => `${C.dim}${C.slate}${line}${C.reset}`)
184
- : [`${C.dim}${C.slate}No thinking trace was recorded.${C.reset}`],
185
- });
243
+ if (request.view === 'thinking') {
244
+ const thinking = String(details.thinking || '').trim();
245
+ printSolidPanel({
246
+ title: `Thinking • ${queryLabel} • ${summary.thinkingCharacters} chars`,
247
+ lines: thinking
248
+ ? thinking.split('\n').flatMap(line => wrapText(sanitizeTerminalText(cleanThinkingLine(line), 500000), width)).map(line => `${C.dim}${C.slate}${line}${C.reset}`)
249
+ : [`${C.dim}${C.slate}No thinking trace was recorded.${C.reset}`],
250
+ });
251
+ return;
252
+ }
186
253
 
187
- if (!details.tools.length) {
254
+ if (request.view === 'tools') {
188
255
  printSolidPanel({
189
- title: `Tools • Q${details.number}`,
190
- lines: [`${C.dim}${C.slate}No tools were used.${C.reset}`],
256
+ title: `Tool Timeline • ${queryLabel} • ${summary.toolCount} calls`,
257
+ lines: [
258
+ ...toolTimelineLines(true),
259
+ '',
260
+ `${C.dim}Open raw data with /details ${queryLabel} tool <n>.${C.reset}`,
261
+ ],
191
262
  });
192
263
  return;
193
264
  }
194
265
 
195
- for (const [index, tool] of details.tools.entries()) {
196
- const args = JSON.stringify(tool.args ?? {}, null, 2);
197
- const serializedResult = JSON.stringify(tool.result ?? {}, null, 2);
198
- const result = serializedResult.length > 6000
199
- ? serializedResult.slice(0, 6000) + '\n… result truncated'
200
- : serializedResult;
201
- const lines = [
202
- ...safeLines('Duration', tool.duration == null ? 'n/a' : `${(tool.duration / 1000).toFixed(2)}s`),
203
- ...safeLines('Arguments', args),
204
- ...safeLines('Result', result),
266
+ if (request.view === 'tool') {
267
+ const tool = details.tools?.[request.toolIndex - 1];
268
+ if (!tool) {
269
+ console.log(`\n ${C.amber}[NOT FOUND] ${queryLabel} has no tool call #${request.toolIndex}.${C.reset}\n`);
270
+ return;
271
+ }
272
+ const args = serializeDetailValue(tool.args, Infinity);
273
+ const result = serializeDetailValue(tool.result, Infinity);
274
+ const rawLines = (label, serialized) => [
275
+ `${C.bold}${C.lavender}${label}:${C.reset}`,
276
+ ...serialized.text.split('\n').flatMap(line => safeWrappedLines(line, C.slate)),
205
277
  ];
206
- printSolidPanel({ title: `Tool ${index + 1} • ${tool.name} • Q${details.number}`, lines });
278
+ printSolidPanel({
279
+ title: `Tool ${request.toolIndex} • ${sanitizeTerminalText(tool.name, 128)} • ${queryLabel}`,
280
+ lines: [
281
+ `${C.gold}Duration:${C.reset} ${C.white}${formatDetailDuration(tool.duration)}${C.reset}`,
282
+ ...rawLines('Arguments', args),
283
+ ...rawLines('Result', result),
284
+ ],
285
+ });
286
+ return;
287
+ }
288
+
289
+ if (request.view === 'markdown' || request.view === 'all') {
290
+ const copyable = getCopyableContent(details, request);
291
+ printSolidPanel({
292
+ title: copyable.title,
293
+ lines: copyable.text.split('\n').flatMap(line => wrapText(line, width)).map(line => `${C.white}${line}${C.reset}`),
294
+ });
295
+ return;
296
+ }
297
+
298
+ printSolidPanel({
299
+ title: `Query Diagnostics • ${queryLabel}`,
300
+ lines: [
301
+ `${C.gold}Status:${C.reset} ${statusColor}${statusLabel}${C.reset}${details.error ? ` ${C.red}• ${sanitizeTerminalText(details.error, 240)}${C.reset}` : ''}`,
302
+ `${C.gold}Runtime:${C.reset} ${C.white}${sanitizeTerminalText(details.provider || 'unknown', 80)} / ${sanitizeTerminalText(details.model || 'unknown', 160)}${C.reset}`,
303
+ `${C.gold}Duration:${C.reset} ${C.white}${formatDetailDuration(details.durationMs)}${C.reset} ${C.dim}• ${details.iterations || 0}/${details.maxIterations || '?'} iterations${C.reset}`,
304
+ `${C.gold}Tools:${C.reset} ${C.white}${summary.toolCount} calls${C.reset} ${C.dim}• ${summary.successfulTools} ok • ${summary.emptyTools} empty • ${summary.failedTools} failed • ${formatDetailDuration(summary.toolDurationMs)}${C.reset}`,
305
+ `${C.gold}Output:${C.reset} ${C.white}${summary.answerCharacters} answer chars${C.reset} ${C.dim}• ${summary.thinkingCharacters} thinking chars • ${details.language || 'n/a'}${C.reset}`,
306
+ `${C.gold}Started:${C.reset} ${C.dim}${startedLabel}${C.reset}`,
307
+ '',
308
+ `${C.bold}${C.lavender}Question:${C.reset}`,
309
+ ...safeWrappedLines(details.prompt),
310
+ '',
311
+ `${C.bold}${C.lavender}Tool timeline:${C.reset}`,
312
+ ...toolTimelineLines(false),
313
+ '',
314
+ `${C.dim}Views: /details ${queryLabel} answer | thinking | tools | tool <n> | all${C.reset}`,
315
+ ],
316
+ });
317
+ }
318
+
319
+ function copyQueryDetails(reference = '') {
320
+ const request = parseDetailsRequest(reference);
321
+ if (request.error) {
322
+ printSolidPanel({
323
+ title: 'Copy Details — Invalid Request',
324
+ lines: [
325
+ `${C.amber}${request.error}${C.reset}`,
326
+ '',
327
+ `${C.white}/copy [Qn] [answer]${C.reset} ${C.dim}— copy model answer (default)${C.reset}`,
328
+ `${C.white}/copy [Qn] thinking${C.reset} ${C.dim}— copy thinking trace${C.reset}`,
329
+ `${C.white}/copy [Qn] tools${C.reset} ${C.dim}— copy tools JSON${C.reset}`,
330
+ `${C.white}/copy [Qn] tool <n>${C.reset} ${C.dim}— copy tool <n> raw result${C.reset}`,
331
+ `${C.white}/copy [Qn] all${C.reset} ${C.dim}— copy full question & answer markdown${C.reset}`,
332
+ ],
333
+ });
334
+ return false;
207
335
  }
336
+
337
+ const details = request.queryNumber == null
338
+ ? lastQueryDetails
339
+ : queryDetails.get(request.queryNumber);
340
+
341
+ if (!details) {
342
+ const target = request.queryNumber == null ? 'the latest query' : `Q${request.queryNumber}`;
343
+ console.log(`\n ${C.amber}[NOT FOUND] No diagnostics recorded for ${target}.${C.reset}\n`);
344
+ return false;
345
+ }
346
+
347
+ const copyable = getCopyableContent(details, request);
348
+ if (!copyable || !copyable.text) {
349
+ console.log(`\n ${C.amber}[EMPTY] Nothing to copy for ${request.view || 'answer'}.${C.reset}\n`);
350
+ return false;
351
+ }
352
+
353
+ const copied = copyToClipboard(copyable.text);
354
+ const lineCount = copyable.text.split('\n').length;
355
+ const charCount = copyable.text.length.toLocaleString();
356
+
357
+ printRichCard({
358
+ title: copied ? 'Copied to Clipboard' : 'Copy Content Ready',
359
+ status: copied ? 'ok' : 'info',
360
+ lines: [
361
+ `• Target: ${copyable.title}`,
362
+ `• Characters: ${charCount} chars (${lineCount} lines)`,
363
+ ...(copied
364
+ ? ['• Status: Copied to system clipboard (Ready to paste with Ctrl+V / Cmd+V)']
365
+ : ['• Status: Clipboard utility unavailable, displaying content below:']),
366
+ ],
367
+ });
368
+
369
+ if (!copied) {
370
+ const width = Math.max(16, getFullWidth() - 8);
371
+ printSolidPanel({
372
+ title: copyable.title,
373
+ lines: copyable.text.split('\n').flatMap(l => wrapText(l, width)).map(l => `${C.white}${l}${C.reset}`),
374
+ });
375
+ }
376
+
377
+ return true;
208
378
  }
209
379
 
210
380
  export function printHelp(specificCmd = null) {
211
381
  if (specificCmd) {
212
- const definition = findCommandDefinition(specificCmd);
213
- if (!definition) {
214
- console.log(`\n ${C.amber}Unknown command: ${specificCmd}${C.reset}`);
215
- console.log(` ${C.dim}Type /help to see all commands.${C.reset}\n`);
382
+ const rawName = specificCmd.trim().toLowerCase();
383
+ const cmdName = rawName.startsWith('/') ? rawName : `/${rawName}`;
384
+ const definition = findCommandDefinition(cmdName);
385
+ const details = getCommandDetails(cmdName);
386
+
387
+ if (!definition && !details) {
388
+ printRichCard({
389
+ title: 'Unknown Command',
390
+ status: 'warning',
391
+ lines: [
392
+ `Command "${specificCmd}" was not found.`,
393
+ 'Type /help to see all available commands.',
394
+ ],
395
+ });
216
396
  return;
217
397
  }
218
- const maxContentWidth = Math.max(40, getFullWidth() - 8);
219
- const wrappedDesc = wrapText(definition.desc, maxContentWidth);
220
- const wrappedUsage = wrapText(`Usage: ${definition.usage}`, maxContentWidth);
398
+
399
+ const titleName = definition?.name || cmdName;
400
+ const summary = details?.summary || definition?.desc || 'Command description';
401
+ const usage = details?.usage || definition?.usage || `${titleName} [options]`;
402
+
403
+ const lines = [
404
+ `${C.bold}${C.white}${summary}${C.reset}`,
405
+ '',
406
+ `${C.bold}${C.lavender}Usage:${C.reset} ${C.cyberCyan}${usage}${C.reset}`,
407
+ ];
408
+
409
+ if (details?.subcommands && details.subcommands.length > 0) {
410
+ lines.push('', `${C.bold}${C.roseGlow}Subcommands & Options:${C.reset}`);
411
+ for (const sub of details.subcommands) {
412
+ lines.push(` ${C.bold}${C.gold}${sub.cmd.padEnd(24)}${C.reset} ${C.white}${sub.desc}${C.reset}`);
413
+ if (sub.example) {
414
+ lines.push(` ${C.dim}↳ Example: ${C.cyberCyan}${sub.example}${C.reset}`);
415
+ }
416
+ }
417
+ }
418
+
419
+ if (details?.examples && details.examples.length > 0) {
420
+ lines.push('', `${C.bold}${C.emerald}Quick Examples:${C.reset}`);
421
+ for (const ex of details.examples) {
422
+ lines.push(` ${C.cyberCyan}❯ ${ex}${C.reset}`);
423
+ }
424
+ }
425
+
221
426
  printSolidPanel({
222
- title: `${UI_ICONS.help} ${definition.name}`,
223
- lines: [
224
- ...wrappedDesc.map(d => `${C.white}${d}${C.reset}`),
225
- ...wrappedUsage.map(u => `${C.dim}${u}${C.reset}`),
226
- ],
427
+ title: `${UI_ICONS.help} Command Help • ${titleName}`,
428
+ lines,
227
429
  });
228
430
  return;
229
431
  }
@@ -247,7 +449,7 @@ export function printHelp(specificCmd = null) {
247
449
  }
248
450
  }
249
451
 
250
- helpLines.push(`${C.dim}Tip: Press Tab for auto-completion • Type /help <command> for details.${C.reset}`);
452
+ helpLines.push('', `${C.dim}Tip: Press Tab for auto-completion • Type /help <command> (e.g. /help suggestion, /help mcp, /help provider) for detailed subcommand breakdowns.${C.reset}`);
251
453
 
252
454
  printSolidPanel({
253
455
  title: `${UI_ICONS.help} Kiku AI Agent — ${COMMAND_DEFINITIONS.length} Commands`,
@@ -302,6 +504,7 @@ class InteractiveTuiPrompt {
302
504
  this.isResolving = false;
303
505
  this.activeResolve = null;
304
506
  this.activeReject = null;
507
+ this.onCancel = options.onCancel || null;
305
508
  this.onSigInt = options.onSigInt || null;
306
509
  this._keyHandler = null;
307
510
  this._renderTimer = null;
@@ -318,18 +521,24 @@ class InteractiveTuiPrompt {
318
521
  }
319
522
 
320
523
  addViewportLines(lines) {
321
- if (!lines) return;
524
+ if (lines === null || lines === undefined) return;
322
525
  if (!Array.isArray(lines)) lines = [lines];
526
+ const wasLive = this.scrollOffset === 0;
323
527
  for (const l of lines) {
324
528
  if (typeof l === 'string') {
325
529
  const sublines = l.split('\n');
326
- for (const sl of sublines) this.viewportLines.push(sl);
530
+ for (const sl of sublines) {
531
+ this.viewportLines.push(sl);
532
+ }
533
+ } else if (l != null) {
534
+ this.viewportLines.push(String(l));
327
535
  }
328
536
  }
329
537
  if (this.viewportLines.length > 5000) {
330
- this.viewportLines.splice(0, this.viewportLines.length - 5000);
538
+ const removedCount = this.viewportLines.length - 5000;
539
+ this.viewportLines.splice(0, removedCount);
331
540
  }
332
- if (this.scrollOffset === 0) {
541
+ if (wasLive) {
333
542
  this.scrollOffset = 0;
334
543
  }
335
544
  this.render();
@@ -351,7 +560,7 @@ class InteractiveTuiPrompt {
351
560
  }
352
561
 
353
562
  clearPrompt() {
354
-
563
+
355
564
  }
356
565
 
357
566
  clearPopover() {
@@ -365,7 +574,7 @@ class InteractiveTuiPrompt {
365
574
  _cleanupInput() {
366
575
  if (this._renderTimer) { clearTimeout(this._renderTimer); this._renderTimer = null; }
367
576
  if (this._keyHandler) { input.removeListener('keypress', this._keyHandler); this._keyHandler = null; }
368
- try { if (input.isTTY) input.setRawMode(false); } catch {}
577
+ try { if (input.isTTY) input.setRawMode(false); } catch { }
369
578
  }
370
579
 
371
580
  async readLine() {
@@ -456,68 +665,84 @@ class InteractiveTuiPrompt {
456
665
  render() {
457
666
  if (this.isDisposed || !this.screenBuffer) return;
458
667
  const termCols = Math.max(20, process.stdout.columns || 80);
459
- const termRows = Math.max(10, process.stdout.rows || 24);
668
+ const {
669
+ termRows,
670
+ viewportStartRow,
671
+ viewportHeight,
672
+ statusBarRow,
673
+ promptRow,
674
+ separatorRow,
675
+ } = getTuiViewportLayout(process.stdout.rows || 24);
460
676
 
461
677
  this.screenBuffer.resize(termCols, termRows);
462
678
  this.screenBuffer.clear(C.bgDark);
463
679
 
464
-
680
+
465
681
  const shortModel = (this.modelName || 'gemma-4-e4b').replace(/^.*\//, '');
466
682
  const modelLabel = `${C.gold}✦ ${shortModel}${C.reset}`;
467
683
  const mcpCount = this.agent?.mcpManager?.servers?.size || 0;
468
684
  const mcpLabel = mcpCount > 0 ? `${C.emerald}● ${mcpCount} MCP${C.reset}` : `${C.slate}○ 0 MCP${C.reset}`;
469
685
 
470
- const headerLeft = ` ${C.bold}${C.sakura}◆ KIKU AGENT${C.reset} ${C.slate}v1.0.5${C.reset}`;
471
- const headerRight = `${modelLabel} ${C.slate}│${C.reset} ${mcpLabel} ${C.slate}│${C.reset} ${C.roseGlow}◆ Sakura${C.reset} `;
686
+ const activeTheme = getTheme ? getTheme() : null;
687
+ const themeName = activeTheme?.label || 'Sakura';
688
+ const headerLeft = ` ${C.bold}${C.sakura}◆ KIKU AGENT${C.reset} ${C.slate}v${PACKAGE_VERSION}${C.reset}`;
689
+ const headerRight = `${modelLabel} ${C.slate}│${C.reset} ${mcpLabel} ${C.slate}│${C.reset} ${C.roseGlow}◆ ${themeName}${C.reset} `;
472
690
  const safeCols = Math.max(1, termCols - 1);
473
691
  const headerPad = Math.max(1, safeCols - getStringDisplayWidth(headerLeft) - getStringDisplayWidth(headerRight));
474
692
  this.screenBuffer.setRow(0, `${headerLeft}${' '.repeat(headerPad)}${headerRight}`, C.bgPanelHeader);
475
693
 
476
694
 
477
- const vpStartRow = 1;
478
- const vpHeight = Math.max(1, termRows - 4);
479
695
  const totalLines = this.viewportLines.length;
480
- const maxOffset = Math.max(0, totalLines - vpHeight);
696
+ const maxOffset = Math.max(0, totalLines - viewportHeight);
481
697
  const currentOffset = Math.min(this.scrollOffset, maxOffset);
482
- const startIndex = Math.max(0, totalLines - vpHeight - currentOffset);
483
- const visibleLines = this.viewportLines.slice(startIndex, startIndex + vpHeight);
698
+ const startIndex = Math.max(0, totalLines - viewportHeight - currentOffset);
699
+ const visibleLines = this.viewportLines.slice(startIndex, startIndex + viewportHeight);
484
700
 
485
- for (let r = 0; r < vpHeight; r++) {
701
+ for (let r = 0; r < viewportHeight; r++) {
486
702
  const lineContent = visibleLines[r] || '';
487
- this.screenBuffer.setRow(vpStartRow + r, lineContent, C.bgDark);
703
+ this.screenBuffer.setRow(viewportStartRow + r, lineContent, C.bgDark);
488
704
  }
489
705
 
490
-
491
- const statusBarRow = termRows - 4;
492
706
  const musicStatus = audioPlayer.getStatus();
493
707
  let statusStyled = '';
494
708
 
495
709
  if (isProcessing) {
496
710
  const frame = spinnerFrames[Math.floor(Date.now() / 70) % spinnerFrames.length];
497
- const stateLabel = stripAnsi(this.agentState || 'Processing...');
498
- const elapsed = this.agentState
499
- ? ` ${((Date.now() - (this.agentStateTime || Date.now())) / 1000).toFixed(1)}s`
711
+ const stateLabel = stripAnsi(this.agentState || 'Processing…');
712
+ const elapsed = this.agentState && this.agentStateTime
713
+ ? ` ${((Date.now() - this.agentStateTime) / 1000).toFixed(1)}s`
500
714
  : '';
501
715
  const queueLabel = queryQueue.length > 0 ? ` Queue: ${queryQueue.length}` : '';
502
716
  const activeLabel = activeQueryNumber == null ? '' : `${C.bold}Q${activeQueryNumber}${C.reset}${C.cyberCyan} • `;
503
- statusStyled = ` ${C.cyberCyan}${frame} ${activeLabel}${stateLabel}${elapsed}${C.reset}${C.slate}${queueLabel}${C.reset}`;
717
+ const statusLeft = ` ${C.cyberCyan}${frame} ${activeLabel}${stateLabel}${elapsed}${C.reset}${C.slate}${queueLabel}${C.reset}`;
718
+ if (currentOffset > 0) {
719
+ const statusRight = `${C.amber}${C.bold}↓ ${currentOffset} more${C.reset} ${C.slate}below (press [End])${C.reset} `;
720
+ const pad = Math.max(1, safeCols - getStringDisplayWidth(statusLeft) - getStringDisplayWidth(statusRight));
721
+ statusStyled = `${statusLeft}${' '.repeat(pad)}${statusRight}`;
722
+ } else {
723
+ statusStyled = statusLeft;
724
+ }
725
+ } else if (currentOffset > 0) {
726
+ const posInfo = startIndex > 0 ? ` ${C.slate}• ↑ ${startIndex} above${C.reset}` : '';
727
+ const statusLeft = ` ${C.amber}${C.bold}↓ ${currentOffset} more${C.reset} ${C.slate}below${C.reset}${posInfo}`;
728
+ const statusRight = `${C.slate}press ${C.white}${C.bold}[End]${C.reset}${C.slate} to return to bottom${C.reset} `;
729
+ const pad = Math.max(1, safeCols - getStringDisplayWidth(statusLeft) - getStringDisplayWidth(statusRight));
730
+ statusStyled = `${statusLeft}${' '.repeat(pad)}${statusRight}`;
504
731
  } else if (musicStatus) {
505
732
  statusStyled = ` ${C.cyberCyan}🎵 ${musicStatus}${C.reset}`;
506
- } else if (currentOffset > 0) {
507
- statusStyled = ` ${C.amber}[↑ SCROLLED +${currentOffset} lines — press [End] to return to live stream]${C.reset}`;
508
733
  } else {
509
- statusStyled = ` ${C.emerald}●${C.reset} ${C.slate}IDLE • Ask anything about Ju Jingyi or type / for commands${C.reset}`;
734
+ statusStyled = ` ${C.emerald}● READY${C.reset}`;
510
735
  }
511
736
 
512
737
  this.screenBuffer.setRow(statusBarRow, statusStyled, C.bgPanelHeader);
513
738
 
514
-
739
+
515
740
  if (this.popoverActive && this.popoverItems.length > 0) {
516
- const maxItems = Math.min(5, Math.max(1, vpHeight - 1));
741
+ const maxItems = Math.min(5, Math.max(1, viewportHeight - 1));
517
742
  const startIdx = Math.max(0, Math.min(this.popoverIndex - 2, this.popoverItems.length - maxItems));
518
743
  const visibleItems = this.popoverItems.slice(startIdx, startIdx + maxItems);
519
744
  const popoverHeight = visibleItems.length + 1;
520
- const popoverStartRow = Math.max(vpStartRow, statusBarRow - popoverHeight);
745
+ const popoverStartRow = Math.max(viewportStartRow, statusBarRow - popoverHeight);
521
746
 
522
747
  this.screenBuffer.setRow(
523
748
  popoverStartRow,
@@ -540,11 +765,10 @@ class InteractiveTuiPrompt {
540
765
  }
541
766
  }
542
767
 
543
-
544
- const promptRow = termRows - 2;
768
+
545
769
  let promptStyled = `${C.bold}${C.sakura}${this.promptText}${C.reset}`;
546
770
  const promptVisualWidth = getStringDisplayWidth(this.promptText);
547
- const maxLineWidth = Math.max(1, termCols - promptVisualWidth - 3);
771
+ const maxLineWidth = Math.max(1, termCols - promptVisualWidth - 2);
548
772
 
549
773
  const lineVisualWidth = getStringDisplayWidth(this.line);
550
774
  const { displayLine, cursorVisualOffset } = sliceLineForDisplay(this.line, this.cursor, maxLineWidth);
@@ -563,22 +787,35 @@ class InteractiveTuiPrompt {
563
787
  trailingStyled = `${C.dim}${C.slate}${placeholder}${C.reset}`;
564
788
  }
565
789
 
566
- const rowStyled = ` ❯ ${promptStyled}${C.white}${displayLine}${trailingStyled}`;
790
+ const rowStyled = ` ${promptStyled}${C.white}${displayLine}${trailingStyled}`;
567
791
  this.screenBuffer.setRow(promptRow, rowStyled, C.bgPanel);
568
792
 
569
- this.screenBuffer.setRow(termRows - 1, '─'.repeat(termCols), C.bgDark);
793
+ this.screenBuffer.setRow(separatorRow, '─'.repeat(termCols), C.bgDark);
570
794
 
571
- const cursorCol = 3 + promptVisualWidth + cursorVisualOffset;
795
+ const cursorCol = 2 + promptVisualWidth + cursorVisualOffset;
572
796
  this.screenBuffer.setCursor(promptRow, cursorCol, true);
573
797
  this.screenBuffer.render();
574
798
  }
575
799
 
576
800
  handleKeypress(str, key) {
577
- if (!key) return;
801
+ if (!key && !str) return;
578
802
 
579
- if (key.ctrl && key.name === 'c') {
580
- if (this.line.length > 0) { this.line = ''; this.cursor = 0; this.updateState(); this.render(); return; }
581
- if (isProcessing) { if (this.onSigInt) this.onSigInt(); return; }
803
+ if ((key?.ctrl && key?.name === 'c') || str === '\x03') {
804
+ if (isProcessing) {
805
+ if (this.onCancel) this.onCancel();
806
+ else if (this.onSigInt) this.onSigInt();
807
+ this.addViewportLines(` ${C.amber}[✓ Cancelled by user]${C.reset}`);
808
+ this.agentState = '';
809
+ this.render();
810
+ return;
811
+ }
812
+ if (this.line.length > 0) {
813
+ this.line = '';
814
+ this.cursor = 0;
815
+ this.updateState();
816
+ this.render();
817
+ return;
818
+ }
582
819
  const now = Date.now();
583
820
  if (!this._lastCtrlCTime || now - this._lastCtrlCTime > 2000) {
584
821
  this._lastCtrlCTime = now;
@@ -589,49 +826,88 @@ class InteractiveTuiPrompt {
589
826
  return;
590
827
  }
591
828
 
592
- if (key.name === 'pageup') {
593
- const termRows = Math.max(10, process.stdout.rows || 24);
594
- const vpHeight = Math.max(1, termRows - 4);
595
- const maxOffset = Math.max(0, this.viewportLines.length - vpHeight);
596
- this.scrollOffset = Math.min(maxOffset, this.scrollOffset + Math.max(1, vpHeight - 2));
597
- this.render();
829
+ if (key?.name === 'escape' || str === '\x1b') {
830
+ if (this.popoverActive) {
831
+ this.popoverActive = false;
832
+ this.popoverItems = [];
833
+ this.ghostText = '';
834
+ this.render();
835
+ return;
836
+ }
837
+ if (this.scrollOffset > 0) {
838
+ this.scrollOffset = 0;
839
+ this.render();
840
+ return;
841
+ }
842
+ if (isProcessing) {
843
+ if (this.onCancel) this.onCancel();
844
+ this.addViewportLines(` ${C.amber}[✓ Cancelled by user]${C.reset}`);
845
+ this.agentState = '';
846
+ this.render();
847
+ return;
848
+ }
598
849
  return;
599
850
  }
600
851
 
601
- if (key.name === 'pagedown') {
602
- const termRows = Math.max(10, process.stdout.rows || 24);
603
- const vpHeight = Math.max(1, termRows - 4);
604
- this.scrollOffset = Math.max(0, this.scrollOffset - Math.max(1, vpHeight - 2));
852
+ if (
853
+ str && (
854
+ str.startsWith('\x1b') ||
855
+ str.includes('\x1b') ||
856
+ str.includes('[<') ||
857
+ str.includes('[M') ||
858
+ /^<[0-9;]*[a-zA-Z]?$/.test(str) ||
859
+ /^[0-9;]+[a-zA-Z]$/.test(str) ||
860
+ /^[0-9;]{2,}$/.test(str) ||
861
+ /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(str)
862
+ )
863
+ ) {
864
+ if (str.includes('[<64;') || str.includes('[64;') || str.includes('<64;') || (key?.name === 'up' && key?.shift)) {
865
+ const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
866
+ const maxOffset = Math.max(0, this.viewportLines.length - viewportHeight);
867
+ this.scrollOffset = Math.min(maxOffset, this.scrollOffset + 3);
868
+ this.render();
869
+ return;
870
+ }
871
+ if (str.includes('[<65;') || str.includes('[65;') || str.includes('<65;') || (key?.name === 'down' && key?.shift)) {
872
+ if (this.scrollOffset > 0) {
873
+ this.scrollOffset = Math.max(0, this.scrollOffset - 3);
874
+ this.render();
875
+ }
876
+ return;
877
+ }
878
+ if (str !== '\r' && str !== '\n' && str !== '\t' && str !== '\x08' && str !== '\x7f') {
879
+ return;
880
+ }
881
+ }
882
+
883
+ if (key?.name === 'pageup') {
884
+ const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
885
+ const maxOffset = Math.max(0, this.viewportLines.length - viewportHeight);
886
+ this.scrollOffset = Math.min(maxOffset, this.scrollOffset + Math.max(1, viewportHeight - 2));
605
887
  this.render();
606
888
  return;
607
889
  }
608
890
 
609
- if (key.ctrl && key.name === 'l') {
891
+ if (key?.name === 'pagedown') {
892
+ const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
893
+ this.scrollOffset = Math.max(0, this.scrollOffset - Math.max(1, viewportHeight - 2));
610
894
  this.render();
611
895
  return;
612
896
  }
613
897
 
614
- if (key.name === 'escape') {
615
- if (this.popoverActive) {
616
- this.popoverActive = false;
617
- this.popoverItems = [];
618
- this.ghostText = '';
619
- this.render();
620
- } else if (this.scrollOffset > 0) {
621
- this.scrollOffset = 0;
622
- this.render();
623
- }
898
+ if (key?.ctrl && key?.name === 'l') {
899
+ this.render();
624
900
  return;
625
901
  }
626
902
 
627
- if (key.name === 'f2') {
903
+ if (key?.name === 'f2') {
628
904
  this.finish('/details');
629
905
  return;
630
906
  }
631
907
 
632
- if (key.name === 'tab') {
908
+ if (key?.name === 'tab' || str === '\t') {
633
909
  if (this.popoverActive && this.popoverItems.length > 0) {
634
- const item = this.popoverItems[this.popoverIndex];
910
+ const item = this.popoverItems[this.popoverIndex] || this.popoverItems[0];
635
911
  if (item) {
636
912
  this.line = item.insert || item.name;
637
913
  this.cursor = item.newCursor !== undefined ? item.newCursor : this.line.length;
@@ -649,25 +925,24 @@ class InteractiveTuiPrompt {
649
925
  return;
650
926
  }
651
927
 
652
- if (key.name === 'return' || key.name === 'enter') {
928
+ if (key?.name === 'return' || key?.name === 'enter' || str === '\r' || str === '\n') {
929
+ let textToSubmit = this.line;
653
930
  if (this.popoverActive && this.popoverItems.length > 0) {
654
- const item = this.popoverItems[this.popoverIndex];
655
- if (item) {
656
- const insertVal = item.insert || item.name;
657
- if (insertVal.endsWith(' ')) {
658
- this.line = insertVal;
659
- this.cursor = item.newCursor !== undefined ? item.newCursor : this.line.length;
660
- this.updateState();
661
- this.render();
662
- return;
931
+ const selected = this.popoverItems[this.popoverIndex] || this.popoverItems[0];
932
+ if (selected) {
933
+ const target = (selected.insert || selected.name || '').trim();
934
+ if (this.line.startsWith('/') && !this.line.includes(' ')) {
935
+ textToSubmit = target;
936
+ } else if (this.line.startsWith('/') && this.line.length < target.length && target.startsWith(this.line.trim())) {
937
+ textToSubmit = target;
663
938
  }
664
- this.line = insertVal;
665
- this.finish(this.line);
666
- return;
667
939
  }
668
940
  }
941
+ this.popoverActive = false;
942
+ this.popoverItems = [];
943
+ this.ghostText = '';
669
944
  this.scrollOffset = 0;
670
- this.finish(this.line);
945
+ this.finish(textToSubmit);
671
946
  return;
672
947
  }
673
948
 
@@ -687,22 +962,21 @@ class InteractiveTuiPrompt {
687
962
  this.render();
688
963
  return;
689
964
  }
690
- const termRows = Math.max(10, process.stdout.rows || 24);
691
- const vpHeight = Math.max(1, termRows - 4);
692
- const maxOffset = Math.max(0, this.viewportLines.length - vpHeight);
965
+ const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
966
+ const maxOffset = Math.max(0, this.viewportLines.length - viewportHeight);
693
967
  this.scrollOffset = Math.min(maxOffset, this.scrollOffset + 1);
694
968
  this.render();
695
969
  return;
696
970
  }
697
971
 
698
- if (key.name === 'down' || (key.ctrl && key.name === 'n')) {
972
+ if (key?.name === 'down' || (key?.ctrl && key?.name === 'n')) {
699
973
  if (this.popoverActive && this.popoverItems.length > 0) {
700
974
  this.popoverIndex = (this.popoverIndex + 1) % this.popoverItems.length;
701
975
  this.updateState();
702
976
  this.render();
703
977
  return;
704
978
  }
705
- if (key.ctrl && this.history.length > 0 && this.historyIndex !== -1) {
979
+ if (key?.ctrl && this.history.length > 0 && this.historyIndex !== -1) {
706
980
  if (this.historyIndex < this.history.length - 1) {
707
981
  this.historyIndex++;
708
982
  this.line = this.history[this.historyIndex] || '';
@@ -722,11 +996,10 @@ class InteractiveTuiPrompt {
722
996
  return;
723
997
  }
724
998
 
725
- if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
999
+ if (key?.name === 'home' || (key?.ctrl && key?.name === 'a')) {
726
1000
  if (this.line === '') {
727
- const termRows = Math.max(10, process.stdout.rows || 24);
728
- const vpHeight = Math.max(1, termRows - 6);
729
- this.scrollOffset = Math.max(0, this.viewportLines.length - vpHeight);
1001
+ const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
1002
+ this.scrollOffset = Math.max(0, this.viewportLines.length - viewportHeight);
730
1003
  this.render();
731
1004
  } else {
732
1005
  this.cursor = 0;
@@ -735,7 +1008,7 @@ class InteractiveTuiPrompt {
735
1008
  return;
736
1009
  }
737
1010
 
738
- if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
1011
+ if (key?.name === 'end' || (key?.ctrl && key?.name === 'e')) {
739
1012
  if (this.line === '') {
740
1013
  this.scrollOffset = 0;
741
1014
  this.render();
@@ -746,18 +1019,22 @@ class InteractiveTuiPrompt {
746
1019
  return;
747
1020
  }
748
1021
 
749
- if (key.name === 'right') { if (this.cursor < this.line.length) { this.cursor = nextGraphemeBoundary(this.line, this.cursor); this.render(); } return; }
750
- if (key.name === 'left') { if (this.cursor > 0) { this.cursor = previousGraphemeBoundary(this.line, this.cursor); this.render(); } return; }
1022
+ if (key?.name === 'right') { if (this.cursor < this.line.length) { this.cursor = nextGraphemeBoundary(this.line, this.cursor); this.render(); } return; }
1023
+ if (key?.name === 'left') { if (this.cursor > 0) { this.cursor = previousGraphemeBoundary(this.line, this.cursor); this.render(); } return; }
751
1024
 
752
- if (key.name === 'backspace') { if (this.cursor > 0) { const previous = previousGraphemeBoundary(this.line, this.cursor); this.line = this.line.slice(0, previous) + this.line.slice(this.cursor); this.cursor = previous; this.updateState(); this.render(); } return; }
753
- if (key.name === 'delete') { if (this.cursor < this.line.length) { const next = nextGraphemeBoundary(this.line, this.cursor); this.line = this.line.slice(0, this.cursor) + this.line.slice(next); this.updateState(); this.render(); } return; }
1025
+ if (key?.name === 'backspace' || str === '\x08' || str === '\x7f') { if (this.cursor > 0) { const previous = previousGraphemeBoundary(this.line, this.cursor); this.line = this.line.slice(0, previous) + this.line.slice(this.cursor); this.cursor = previous; this.updateState(); this.render(); } return; }
1026
+ if (key?.name === 'delete') { if (this.cursor < this.line.length) { const next = nextGraphemeBoundary(this.line, this.cursor); this.line = this.line.slice(0, this.cursor) + this.line.slice(next); this.updateState(); this.render(); } return; }
754
1027
 
755
- if (str && !key.ctrl && !key.meta) {
1028
+ if (str && !key?.ctrl && !key?.meta) {
756
1029
  if (
757
1030
  str.startsWith('\x1b') ||
758
- /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(str) ||
759
- /^[0-9;]+[Mm]$/.test(str) ||
760
- /^<*[0-9;]+[Mm]/.test(str)
1031
+ str.includes('\x1b') ||
1032
+ str.includes('[<') ||
1033
+ str.includes('[M') ||
1034
+ /[\x00-\x1f\x7f]/.test(str) ||
1035
+ /^<[0-9;]*[a-zA-Z]?$/.test(str) ||
1036
+ /^[0-9;]+[a-zA-Z]$/.test(str) ||
1037
+ /^[0-9;]{2,}$/.test(str)
761
1038
  ) return;
762
1039
  this.line = this.line.slice(0, this.cursor) + str + this.line.slice(this.cursor);
763
1040
  this.cursor += str.length;
@@ -815,10 +1092,19 @@ function getSuggestionsForLine(line, cursor = (line || '').length, agent = null)
815
1092
 
816
1093
  if (parts.length > 1 || (parts.length === 1 && line.includes(' '))) {
817
1094
  const subs = SUB_COMMANDS[baseCmd] || [];
1095
+ const details = getCommandDetails(baseCmd);
818
1096
  const query = parts.slice(1).join(' ').toLowerCase();
819
1097
  return subs
820
1098
  .filter(s => s.toLowerCase().includes(query))
821
- .map(s => ({ name: `${baseCmd} ${s}`, desc: '', insert: `${baseCmd} ${s} ` }));
1099
+ .map(s => {
1100
+ const subKey = s.split(/\s+/)[0].toLowerCase();
1101
+ const matchedDetail = details?.subcommands?.find(sc => {
1102
+ const scKey = sc.cmd.split(/\s+/)[0].toLowerCase();
1103
+ return scKey === subKey || sc.cmd.toLowerCase().includes(subKey);
1104
+ });
1105
+ const desc = matchedDetail?.desc || '';
1106
+ return { name: `${baseCmd} ${s}`, desc, insert: `${baseCmd} ${s} ` };
1107
+ });
822
1108
  }
823
1109
 
824
1110
  return allCmds
@@ -826,589 +1112,908 @@ function getSuggestionsForLine(line, cursor = (line || '').length, agent = null)
826
1112
  .filter(c => c.score > 0)
827
1113
  .sort((a, b) => b.score - a.score);
828
1114
  }
829
-
830
1115
  async function executeQueryWithUI(agent, prompt, isThinkingVisible = false, signal = null, queryNumber = null) {
831
- let spinnerInterval = null;
832
- let hasPrintedResponseHeader = false;
833
- let responsePanelStarted = false;
834
- let toolSectionStarted = false;
835
- let responseLineBuffer = '';
836
- let pendingTableHeader = null;
837
- let activeTableHeaders = null;
838
- let tableRowNumber = 0;
839
- const boxWidth = getFullWidth();
840
- const bg = '\x1b[48;2;34;37;48m';
841
- const bgHeader = '\x1b[48;2;48;52;68m';
842
- const colorizeResponseText = (formatted) => (
843
- `${C.white}${formatted.replace(/\x1b\[0m/g, C.reset + C.white)}${C.reset}`
844
- );
845
- const queryTag = queryNumber == null ? '' : `Q${queryNumber}`;
846
- const answerTitle = queryTag
847
- ? `Answer • ${queryTag} — ${prompt.length > 52 ? prompt.slice(0, 52) + '…' : prompt}`
848
- : 'Answer';
849
-
850
- const ensureResponsePanel = () => {
851
- if (responsePanelStarted) return;
852
- console.log('');
853
- printSolidBannerRow(` ${C.bold}${C.roseGlow}${answerTitle}${C.reset}`, boxWidth, C.bgPanelHeader);
854
- responsePanelStarted = true;
855
- };
1116
+ const queryStartedAt = Date.now();
1117
+ let spinnerInterval = null;
1118
+ let hasPrintedResponseHeader = false;
1119
+ let responsePanelStarted = false;
1120
+ let toolSectionStarted = false;
1121
+ let responseLineBuffer = '';
1122
+ let pendingTableHeader = null;
1123
+ let activeTableHeaders = null;
1124
+ let tableRowNumber = 0;
1125
+ const boxWidth = getFullWidth();
1126
+ const bg = C.bgPanel;
1127
+ const bgHeader = C.bgPanelHeader;
1128
+ const colorizeResponseText = (formatted) => (
1129
+ `${C.white}${formatted.replace(/\x1b\[0m/g, C.reset + C.white)}${C.reset}`
1130
+ );
1131
+ const queryTag = queryNumber == null ? '' : `Q${queryNumber}`;
1132
+ const answerTitle = queryTag
1133
+ ? `Answer • ${queryTag} — ${prompt}`
1134
+ : 'Answer';
1135
+
1136
+
1137
+
1138
+ let jsonSuppressDepth = 0;
1139
+ let jsonSuppressActive = false;
1140
+ let fencedJsonBlock = false;
1141
+ let suppressedLineCount = 0;
1142
+
1143
+ const _RAW_JSON_FIELD_RE = /["'](?:_meta|protocol|requestedTool|authoritative|tool_call_id|validationWarnings|cache|duplicate_call_in_query)["']\s*:/;
1144
+ const _ACTION_INPUT_RE = /^Action\s*:\s*[a-zA-Z0-9_]+\s*$/;
1145
+ const _ACTION_INPUT_BODY_RE = /^Action Input\s*:\s*\{/;
1146
+
1147
+ const isRawJsonPayloadLine = (line) => {
1148
+ const trimmed = line.trim();
1149
+ if (/^```json\s*$/i.test(trimmed)) return 'fence_start';
1150
+ if (fencedJsonBlock && /^```\s*$/.test(trimmed)) return 'fence_end';
1151
+ if (_RAW_JSON_FIELD_RE.test(trimmed)) return 'json_field';
1152
+ if (_ACTION_INPUT_RE.test(trimmed) || _ACTION_INPUT_BODY_RE.test(trimmed)) return 'action_pattern';
1153
+ if (jsonSuppressActive) {
1154
+ if (/^[{}\[\],]*$/.test(trimmed)) return 'json_structure';
1155
+ if (/^["'][a-zA-Z_]+["']\s*:/.test(trimmed)) return 'json_field';
1156
+ if (/^(?:\d+|true|false|null|"[^"]*")\s*,?\s*$/.test(trimmed)) return 'json_value';
1157
+ }
1158
+ return null;
1159
+ };
856
1160
 
857
- const printPlainResponseLine = (line) => {
858
- ensureResponsePanel();
859
- const wrapSafeLine = expandMarkdownLinks(line);
860
- const formatted = formatTerminalMarkdown(wrapSafeLine);
861
- const maxWidth = getFullWidth() - 4;
862
- const visualLen = getStringDisplayWidth(formatted);
863
- if (visualLen > maxWidth) {
864
- for (const wl of wrapText(wrapSafeLine, maxWidth)) {
865
- printSolidBannerRow(` ${colorizeResponseText(formatTerminalMarkdown(wl))}`, boxWidth, C.bgPanel);
866
- }
867
- } else {
868
- printSolidBannerRow(` ${colorizeResponseText(formatted)}`, boxWidth, C.bgPanel);
869
- }
870
- };
1161
+ const filterResponseLine = (line) => {
1162
+ const detection = isRawJsonPayloadLine(line);
871
1163
 
872
- const printTableRecord = (cells) => {
873
- ensureResponsePanel();
874
- tableRowNumber++;
875
- printSolidBannerRow(` ${C.bold}${C.cyberCyan}Item ${tableRowNumber}${C.reset}`, boxWidth, C.bgPanelHeader);
876
-
877
- const maxWidth = Math.max(16, getFullWidth() - 8);
878
- for (let index = 0; index < activeTableHeaders.length; index++) {
879
- const header = activeTableHeaders[index] || `Column ${index + 1}`;
880
- const value = expandMarkdownLinks(cells[index] || '—');
881
- const prefix = `${header}: `;
882
- const wrapped = wrapText(prefix + value, maxWidth);
883
- for (const line of wrapped) {
884
- printSolidBannerRow(` ${colorizeResponseText(formatTerminalMarkdown(line))}`, boxWidth, C.bgPanel);
885
- }
886
- }
887
- };
1164
+ if (detection === 'fence_start') {
1165
+ fencedJsonBlock = true;
1166
+ jsonSuppressActive = true;
1167
+ suppressedLineCount = 0;
1168
+ return false;
1169
+ }
1170
+ if (detection === 'fence_end') {
1171
+ fencedJsonBlock = false;
1172
+ jsonSuppressActive = false;
1173
+ if (suppressedLineCount > 0) {
1174
+ printPlainResponseLine(`${C.dim}${C.slate}[tool data filtered — ${suppressedLineCount} lines]${C.reset}`);
1175
+ }
1176
+ suppressedLineCount = 0;
1177
+ return false;
1178
+ }
1179
+ if (fencedJsonBlock) {
1180
+ suppressedLineCount++;
1181
+ return false;
1182
+ }
888
1183
 
889
- const flushPendingTableHeader = () => {
890
- if (!pendingTableHeader) return;
891
- printPlainResponseLine(`| ${pendingTableHeader.join(' | ')} |`);
892
- pendingTableHeader = null;
893
- };
1184
+ if (detection) {
1185
+ if (!jsonSuppressActive) {
1186
+ jsonSuppressActive = true;
1187
+ jsonSuppressDepth = 0;
1188
+ suppressedLineCount = 0;
1189
+ }
1190
+ const trimmed = line.trim();
1191
+ for (const ch of trimmed) {
1192
+ if (ch === '{' || ch === '[') jsonSuppressDepth++;
1193
+ else if (ch === '}' || ch === ']') jsonSuppressDepth--;
1194
+ }
1195
+ suppressedLineCount++;
1196
+ if (jsonSuppressDepth <= 0 && suppressedLineCount > 1) {
1197
+ jsonSuppressActive = false;
1198
+ if (suppressedLineCount > 2) {
1199
+ printPlainResponseLine(`${C.dim}${C.slate}[tool data filtered — ${suppressedLineCount} lines]${C.reset}`);
1200
+ }
1201
+ suppressedLineCount = 0;
1202
+ }
1203
+ return false;
1204
+ }
894
1205
 
895
- const printResponseLine = (line) => {
896
- const cells = parseMarkdownTableRow(line);
1206
+ if (jsonSuppressActive) {
1207
+ jsonSuppressActive = false;
1208
+ jsonSuppressDepth = 0;
1209
+ if (suppressedLineCount > 2) {
1210
+ printPlainResponseLine(`${C.dim}${C.slate}[tool data filtered — ${suppressedLineCount} lines]${C.reset}`);
1211
+ }
1212
+ suppressedLineCount = 0;
1213
+ }
897
1214
 
898
- if (pendingTableHeader) {
899
- if (cells && cells.length === pendingTableHeader.length && isMarkdownTableSeparator(cells)) {
900
- activeTableHeaders = pendingTableHeader;
901
- pendingTableHeader = null;
902
- tableRowNumber = 0;
903
- return;
904
- }
905
- flushPendingTableHeader();
906
- }
1215
+ return true;
1216
+ };
1217
+
1218
+ const ensureResponsePanel = () => {
1219
+ if (responsePanelStarted) return;
1220
+ console.log('');
1221
+ printSolidBannerRow(` ${C.bold}${C.roseGlow}${answerTitle}${C.reset}`, boxWidth, C.bgPanelHeader);
1222
+ responsePanelStarted = true;
1223
+ };
1224
+
1225
+ const printPlainResponseLine = (line) => {
1226
+ ensureResponsePanel();
1227
+ const wrapSafeLine = expandMarkdownLinks(line);
1228
+ const formatted = formatTerminalMarkdown(wrapSafeLine);
1229
+ const maxWidth = getFullWidth() - 4;
1230
+ const visualLen = getStringDisplayWidth(formatted);
1231
+ if (visualLen > maxWidth) {
1232
+ for (const wl of wrapText(wrapSafeLine, maxWidth)) {
1233
+ printSolidBannerRow(` ${colorizeResponseText(formatTerminalMarkdown(wl))}`, boxWidth, C.bgPanel);
1234
+ }
1235
+ } else {
1236
+ printSolidBannerRow(` ${colorizeResponseText(formatted)}`, boxWidth, C.bgPanel);
1237
+ }
1238
+ };
1239
+
1240
+ const printTableRecord = (cells) => {
1241
+ ensureResponsePanel();
1242
+ tableRowNumber++;
1243
+ printSolidBannerRow(` ${C.bold}${C.cyberCyan}Item ${tableRowNumber}${C.reset}`, boxWidth, C.bgPanelHeader);
1244
+
1245
+ const maxWidth = Math.max(16, getFullWidth() - 8);
1246
+ for (let index = 0; index < activeTableHeaders.length; index++) {
1247
+ const header = activeTableHeaders[index] || `Column ${index + 1}`;
1248
+ const value = expandMarkdownLinks(cells[index] || '—');
1249
+ const prefix = `${header}: `;
1250
+ const wrapped = wrapText(prefix + value, maxWidth);
1251
+ for (const line of wrapped) {
1252
+ printSolidBannerRow(` ${colorizeResponseText(formatTerminalMarkdown(line))}`, boxWidth, C.bgPanel);
1253
+ }
1254
+ }
1255
+ };
907
1256
 
908
- if (activeTableHeaders) {
909
- if (cells && cells.length === activeTableHeaders.length && !isMarkdownTableSeparator(cells)) {
910
- printTableRecord(cells);
911
- return;
912
- }
913
- activeTableHeaders = null;
914
- tableRowNumber = 0;
915
- }
1257
+ const flushPendingTableHeader = () => {
1258
+ if (!pendingTableHeader) return;
1259
+ printPlainResponseLine(`| ${pendingTableHeader.join(' | ')} |`);
1260
+ pendingTableHeader = null;
1261
+ };
916
1262
 
917
- if (cells && !isMarkdownTableSeparator(cells)) {
918
- pendingTableHeader = cells;
919
- return;
920
- }
1263
+ const printResponseLine = (line) => {
1264
+ if (!filterResponseLine(line)) return;
921
1265
 
922
- printPlainResponseLine(line);
923
- };
1266
+ const cells = parseMarkdownTableRow(line);
924
1267
 
925
- const flushResponseBuffer = (isFinal = false) => {
926
- if (responseLineBuffer.length > 0) {
927
- if (isFinal) {
928
- for (const line of responseLineBuffer.split('\n')) { if (line.length > 0) printResponseLine(line); }
929
- responseLineBuffer = '';
930
- } else if (responseLineBuffer.includes('\n')) {
931
- const lines = responseLineBuffer.split('\n');
932
- responseLineBuffer = lines.pop();
933
- for (const line of lines) printResponseLine(line);
934
- }
935
- }
936
- if (isFinal) flushPendingTableHeader();
937
- };
1268
+ if (pendingTableHeader) {
1269
+ if (cells && cells.length === pendingTableHeader.length && isMarkdownTableSeparator(cells)) {
1270
+ activeTableHeaders = pendingTableHeader;
1271
+ pendingTableHeader = null;
1272
+ tableRowNumber = 0;
1273
+ return;
1274
+ }
1275
+ flushPendingTableHeader();
1276
+ }
938
1277
 
939
- const startSpinner = (label) => {
940
- if (globalTuiPrompt) { globalTuiPrompt.agentState = label; globalTuiPrompt.agentStateTime = Date.now(); }
941
- if (!spinnerInterval) {
942
- spinnerInterval = setInterval(() => { if (globalTuiPrompt && isProcessing) globalTuiPrompt.render(); }, 80);
943
- }
944
- };
1278
+ if (activeTableHeaders) {
1279
+ if (cells && cells.length === activeTableHeaders.length && !isMarkdownTableSeparator(cells)) {
1280
+ printTableRecord(cells);
1281
+ return;
1282
+ }
1283
+ activeTableHeaders = null;
1284
+ tableRowNumber = 0;
1285
+ }
945
1286
 
946
- const stopSpinner = () => {
947
- if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
948
- if (globalTuiPrompt) { globalTuiPrompt.agentState = ''; globalTuiPrompt.render(); }
949
- };
1287
+ if (cells && !isMarkdownTableSeparator(cells)) {
1288
+ pendingTableHeader = cells;
1289
+ return;
1290
+ }
950
1291
 
951
- let thinkingShown = false;
952
- let thinkingBuffer = '';
953
- let thinkingStartTime = 0;
954
- let accumulatedThinking = '';
955
- let recordedTools = [];
956
- let currentToolArgs = {};
1292
+ printPlainResponseLine(line);
1293
+ };
1294
+
1295
+ const flushResponseBuffer = (isFinal = false) => {
1296
+ if (responseLineBuffer.length > 0) {
1297
+ if (isFinal) {
1298
+ for (const line of responseLineBuffer.split('\n')) { if (line.length > 0) printResponseLine(line); }
1299
+ responseLineBuffer = '';
1300
+ } else if (responseLineBuffer.includes('\n')) {
1301
+ const lines = responseLineBuffer.split('\n');
1302
+ responseLineBuffer = lines.pop();
1303
+ for (const line of lines) printResponseLine(line);
1304
+ }
1305
+ }
1306
+ if (isFinal) {
1307
+ if (jsonSuppressActive && suppressedLineCount > 2) {
1308
+ printPlainResponseLine(`${C.dim}${C.slate}[tool data filtered — ${suppressedLineCount} lines]${C.reset}`);
1309
+ }
1310
+ jsonSuppressActive = false;
1311
+ jsonSuppressDepth = 0;
1312
+ suppressedLineCount = 0;
1313
+ fencedJsonBlock = false;
1314
+ flushPendingTableHeader();
1315
+ }
1316
+ };
957
1317
 
958
- startSpinner(`${C.slate}Thinking…${C.reset}`);
1318
+ const startSpinner = (label) => {
1319
+ if (globalTuiPrompt) {
1320
+ globalTuiPrompt.agentState = label;
1321
+ globalTuiPrompt.agentStateTime = Date.now();
1322
+ globalTuiPrompt.render();
1323
+ }
1324
+ if (!spinnerInterval) {
1325
+ spinnerInterval = setInterval(() => {
1326
+ if (globalTuiPrompt && isProcessing) {
1327
+ globalTuiPrompt.render();
1328
+ }
1329
+ }, 80);
1330
+ }
1331
+ };
959
1332
 
960
- try {
961
- await agent.ask(prompt, {
962
- signal,
963
- onStepStart: () => {},
964
- onReasoningStart: () => {
965
- stopSpinner();
966
- thinkingStartTime = Date.now();
967
- thinkingBuffer = '';
968
- if (!thinkingShown) {
969
- console.log('');
970
- const icon = thinkingFrames[Math.floor(Date.now() / 200) % thinkingFrames.length];
971
- printSolidBannerRow(` ${C.lavender}${C.bold}${icon} Thinking${queryTag ? ` • ${queryTag}` : ''}${C.reset}`, boxWidth, bgHeader);
972
- }
973
- },
974
- onReasoningDelta: (chunk) => {
975
- accumulatedThinking += chunk;
976
- thinkingBuffer += chunk;
977
- if (!thinkingShown) {
978
- const lines = thinkingBuffer.split('\n');
979
- thinkingBuffer = lines.pop();
980
- for (const line of lines) {
981
- if (line.trim()) {
982
- for (const w of wrapText(cleanThinkingLine(line), boxWidth - 6)) {
983
- printSolidBannerRow(` ${C.dim}${C.slate}${w}${C.reset}`, boxWidth, bg);
984
- }
1333
+ const stopSpinner = () => {
1334
+ if (spinnerInterval) {
1335
+ clearInterval(spinnerInterval);
1336
+ spinnerInterval = null;
985
1337
  }
986
- }
987
- }
988
- },
989
- onReasoningEnd: () => {
990
- const elapsed = ((Date.now() - thinkingStartTime) / 1000).toFixed(1);
991
- if (!thinkingShown) {
992
- if (thinkingBuffer.trim()) {
993
- for (const w of wrapText(cleanThinkingLine(thinkingBuffer), boxWidth - 6)) {
994
- printSolidBannerRow(` ${C.dim}${C.slate}${w}${C.reset}`, boxWidth, bg);
1338
+ if (globalTuiPrompt) {
1339
+ globalTuiPrompt.agentState = '';
1340
+ globalTuiPrompt.render();
995
1341
  }
996
- }
997
- printSolidBannerRow(` ${C.emerald}[DONE] Thought for ${elapsed}s${C.reset}`, boxWidth, bg);
998
- console.log('');
999
- } else {
1000
- stopSpinner();
1001
- const detailHint = queryTag ? ` — F2 or /details ${queryTag}` : '';
1002
- console.log(`\n ${C.dim}${C.slate}Thinking${queryTag ? ` • ${queryTag}` : ''} (${elapsed}s)${detailHint}${C.reset}`);
1003
- }
1004
- thinkingShown = true;
1005
- },
1006
-
1007
- onToolStart: (toolName, args) => {
1008
- stopSpinner();
1009
- if (!toolSectionStarted) {
1010
- console.log('');
1011
- const detailHint = queryTag ? ` ${C.dim}Details: F2 or /details ${queryTag}${C.reset}` : '';
1012
- printSolidBannerRow(` ${C.bold}${C.cyberCyan}Tools${queryTag ? ` • ${queryTag}` : ''}${C.reset}${detailHint}`, boxWidth, C.bgPanelHeader);
1013
- toolSectionStarted = true;
1014
- }
1015
- currentToolArgs = args || {};
1016
- const queryVal = args?.query || args?.url || Object.values(args || {})[0] || '';
1017
- const displayQuery = typeof queryVal === 'string' && queryVal.length > 30 ? queryVal.slice(0, 30) + '…' : queryVal;
1018
- startSpinner(`${C.cyberCyan}• ${C.bold}${C.white}${toolName}${C.reset}${displayQuery ? ` ${C.slate}"${displayQuery}"${C.reset}` : ''}`);
1019
- },
1020
-
1021
- onToolResult: (toolName, result, durationMs) => {
1022
- stopSpinner();
1023
- recordedTools.push({ name: toolName, args: currentToolArgs, result, duration: durationMs });
1024
-
1025
- const count = result.count ?? result.returnedCount;
1026
- const isError = result.status === 'error';
1027
- const isNotFound = result.status === 'not_found' || count === 0;
1028
- const statusLabel = isError
1029
- ? `${C.red}[ERROR]`
1030
- : isNotFound
1031
- ? `${C.amber}[NOT FOUND]`
1032
- : `${C.emerald}[OK]`;
1033
- const countTag = count != null && !isError ? ` • ${C.cyberCyan}${count} results${C.reset}` : '';
1034
- const timerTag = durationMs !== undefined ? ` • ${C.dim}${(durationMs / 1000).toFixed(2)}s${C.reset}` : '';
1035
-
1036
- printSolidBannerRow(` ${C.slate}• ${C.bold}${C.cyberCyan}${toolName}${C.reset} ${statusLabel} ${(isError ? C.red + 'Failed' : isNotFound ? C.amber + 'No matching data' : C.white + 'OK') + C.reset}${countTag}${timerTag}`, boxWidth, C.bgPanel);
1037
-
1038
- if (!isError && !isNotFound) {
1039
- const preview = extractPreview(result, toolName);
1040
- if (preview) {
1041
- for (const pLine of wrapText('• ' + preview, boxWidth - 6)) {
1042
- printSolidBannerRow(` ${C.dim}${C.slate}${pLine}${C.reset}`, boxWidth, bg);
1342
+ };
1343
+
1344
+ let thinkingShown = false;
1345
+ let thinkingBuffer = '';
1346
+ let thinkingStartTime = 0;
1347
+ let accumulatedThinking = '';
1348
+ let recordedTools = [];
1349
+ let currentToolArgs = {};
1350
+ let completedIterations = 0;
1351
+ let finalAnswer = '';
1352
+ let finalMeta = {};
1353
+
1354
+ const persistQueryDetails = (status, error = null) => {
1355
+ lastQueryDetails = {
1356
+ number: queryNumber,
1357
+ prompt,
1358
+ answer: finalAnswer,
1359
+ thinking: accumulatedThinking,
1360
+ tools: recordedTools,
1361
+ status,
1362
+ error: error ? String(error.message || error) : null,
1363
+ provider: agent.client?.providerName || agent.client?.provider || 'unknown',
1364
+ model: agent.client?.model || 'unknown',
1365
+ language: finalMeta.language || null,
1366
+ iterations: completedIterations,
1367
+ maxIterations: agent.maxIterations,
1368
+ startedAt: queryStartedAt,
1369
+ completedAt: Date.now(),
1370
+ durationMs: finalMeta.durationMs ?? (Date.now() - queryStartedAt),
1371
+ timestamp: Date.now(),
1372
+ };
1373
+ if (queryNumber != null) {
1374
+ queryDetails.set(queryNumber, lastQueryDetails);
1375
+ if (queryDetails.size > 50) queryDetails.delete(queryDetails.keys().next().value);
1043
1376
  }
1377
+ };
1378
+
1379
+ startSpinner(`${C.slate}Thinking…${C.reset}`);
1380
+
1381
+ try {
1382
+ const returnedAnswer = await agent.ask(prompt, {
1383
+ signal,
1384
+ onStepStart: iteration => { completedIterations = Math.max(completedIterations, Number(iteration) || 0); },
1385
+ onReasoningStart: () => {
1386
+ startSpinner(`${C.slate}Thinking…${C.reset}`);
1387
+ thinkingStartTime = Date.now();
1388
+ thinkingBuffer = '';
1389
+ if (!thinkingShown) {
1390
+ console.log('');
1391
+ const icon = thinkingFrames[Math.floor(Date.now() / 200) % thinkingFrames.length];
1392
+ printSolidBannerRow(` ${C.lavender}${C.bold}${icon} Thinking${queryTag ? ` • ${queryTag}` : ''}${C.reset}`, boxWidth, bgHeader);
1393
+ }
1394
+ },
1395
+ onReasoningDelta: (chunk) => {
1396
+ accumulatedThinking += chunk;
1397
+ thinkingBuffer += chunk;
1398
+ if (!thinkingShown) {
1399
+ const lines = thinkingBuffer.split('\n');
1400
+ thinkingBuffer = lines.pop();
1401
+ for (const line of lines) {
1402
+ if (line.trim()) {
1403
+ for (const w of wrapText(cleanThinkingLine(line), boxWidth - 6)) {
1404
+ printSolidBannerRow(` ${C.dim}${C.slate}${w}${C.reset}`, boxWidth, bg);
1405
+ }
1406
+ }
1407
+ }
1408
+ }
1409
+ },
1410
+ onReasoningEnd: () => {
1411
+ const elapsed = ((Date.now() - thinkingStartTime) / 1000).toFixed(1);
1412
+ if (!thinkingShown) {
1413
+ if (thinkingBuffer.trim()) {
1414
+ for (const w of wrapText(cleanThinkingLine(thinkingBuffer), boxWidth - 6)) {
1415
+ printSolidBannerRow(` ${C.dim}${C.slate}${w}${C.reset}`, boxWidth, bg);
1416
+ }
1417
+ }
1418
+ printSolidBannerRow(` ${C.emerald}[DONE] Thought for ${elapsed}s${C.reset}`, boxWidth, bg);
1419
+ console.log('');
1420
+ } else {
1421
+ const detailHint = queryTag ? ` — F2 or /details ${queryTag}` : '';
1422
+ console.log(`\n ${C.dim}${C.slate}Thinking${queryTag ? ` • ${queryTag}` : ''} (${elapsed}s)${detailHint}${C.reset}`);
1423
+ }
1424
+ thinkingShown = true;
1425
+ },
1426
+
1427
+ onToolStart: (toolName, args) => {
1428
+ if (!toolSectionStarted) {
1429
+ console.log('');
1430
+ const detailHint = queryTag ? ` ${C.dim}Details: F2 or /details ${queryTag}` : '';
1431
+ printSolidBannerRow(` ${C.bold}${C.cyberCyan}Tools${queryTag ? ` • ${queryTag}` : ''}${C.reset}${detailHint}`, boxWidth, C.bgPanelHeader);
1432
+ toolSectionStarted = true;
1433
+ }
1434
+ currentToolArgs = args || {};
1435
+ const queryVal = args?.query || args?.url || Object.values(args || {})[0] || '';
1436
+ const displayQuery = typeof queryVal === 'string' ? queryVal : '';
1437
+ startSpinner(`${C.cyberCyan}• ${C.bold}${C.white}${toolName}${C.reset}${displayQuery ? ` ${C.slate}"${displayQuery}"${C.reset}` : ''}`);
1438
+ },
1439
+
1440
+ onToolResult: (toolName, result, durationMs) => {
1441
+ recordedTools.push({ name: toolName, args: currentToolArgs, result, duration: durationMs });
1442
+
1443
+ const count = result.count ?? result.returnedCount;
1444
+ const isError = result.status === 'error';
1445
+ const isNotFound = result.status === 'not_found' || count === 0;
1446
+ const statusLabel = isError
1447
+ ? `${C.red}[ERROR]`
1448
+ : isNotFound
1449
+ ? `${C.amber}[NOT FOUND]`
1450
+ : `${C.emerald}[OK]`;
1451
+ const countTag = count != null && !isError ? ` • ${C.cyberCyan}${count} results${C.reset}` : '';
1452
+ const timerTag = durationMs !== undefined ? ` • ${C.dim}${(durationMs / 1000).toFixed(2)}s${C.reset}` : '';
1453
+
1454
+ printSolidBannerRow(` ${C.slate}• ${C.bold}${C.cyberCyan}${toolName}${C.reset} ${statusLabel} ${(isError ? C.red + 'Failed' : isNotFound ? C.amber + 'No matching data' : C.white + 'OK') + C.reset}${countTag}${timerTag}`, boxWidth, C.bgPanel);
1455
+
1456
+ if (!isError && !isNotFound) {
1457
+ const preview = extractPreview(result, toolName);
1458
+ if (preview) {
1459
+ for (const pLine of wrapText('• ' + preview, boxWidth - 6)) {
1460
+ printSolidBannerRow(` ${C.dim}${C.slate}${pLine}${C.reset}`, boxWidth, bg);
1461
+ }
1462
+ }
1463
+ }
1464
+ },
1465
+
1466
+ onTokenStart: () => {
1467
+ if (globalTuiPrompt) {
1468
+ globalTuiPrompt.agentState = 'Streaming…';
1469
+ globalTuiPrompt.agentStateTime = Date.now();
1470
+ globalTuiPrompt.render();
1471
+ }
1472
+ },
1473
+
1474
+ onTokenDelta: (token) => {
1475
+ if (!hasPrintedResponseHeader) { hasPrintedResponseHeader = true; }
1476
+ responseLineBuffer += token;
1477
+ flushResponseBuffer(false);
1478
+ },
1479
+
1480
+ onFinalAnswer: (answer, meta = {}) => {
1481
+ stopSpinner();
1482
+ finalAnswer = String(answer || '');
1483
+ finalMeta = meta || {};
1484
+ if (!hasPrintedResponseHeader) {
1485
+ for (const line of (answer || '').split('\n')) { if (line.length > 0) printResponseLine(line); }
1486
+ flushPendingTableHeader();
1487
+ } else {
1488
+ flushResponseBuffer(true);
1489
+ }
1490
+ },
1491
+
1492
+ onError: (err) => {
1493
+ stopSpinner();
1494
+ printRichCard({
1495
+ title: 'Execution Error',
1496
+ status: 'error',
1497
+ lines: [err.message],
1498
+ });
1499
+ },
1500
+ });
1501
+
1502
+ if (!finalAnswer && returnedAnswer) finalAnswer = String(returnedAnswer);
1503
+ persistQueryDetails('complete');
1504
+ if (responsePanelStarted) console.log('');
1505
+ } catch (err) {
1506
+ stopSpinner();
1507
+ persistQueryDetails(isAbortError(err) ? 'aborted' : 'error', err);
1508
+ if (!isAbortError(err)) {
1509
+ printRichCard({
1510
+ title: 'Query Failed',
1511
+ status: 'error',
1512
+ lines: [err.message],
1513
+ });
1514
+ } else {
1515
+ printRichCard({
1516
+ title: 'Query Cancelled',
1517
+ status: 'warning',
1518
+ lines: ['Query processing was interrupted.'],
1519
+ });
1520
+ }
1521
+ } finally {
1522
+ stopSpinner();
1044
1523
  }
1045
1524
  }
1046
- },
1047
-
1048
- onTokenStart: () => { stopSpinner(); },
1049
-
1050
- onTokenDelta: (token) => {
1051
- if (!hasPrintedResponseHeader) { stopSpinner(); hasPrintedResponseHeader = true; }
1052
- responseLineBuffer += token;
1053
- flushResponseBuffer(false);
1054
- },
1055
-
1056
- onFinalAnswer: (answer) => {
1057
- stopSpinner();
1058
- if (!hasPrintedResponseHeader) {
1059
- for (const line of (answer || '').split('\n')) { if (line.length > 0) printResponseLine(line); }
1060
- flushPendingTableHeader();
1061
- } else {
1062
- flushResponseBuffer(true);
1063
- }
1064
- },
1065
-
1066
- onError: (err) => {
1067
- stopSpinner();
1068
- console.log(`\n ${C.red}[ERROR] ${err.message}${C.reset}\n`);
1069
- },
1070
- });
1071
-
1072
- lastQueryDetails = { number: queryNumber, prompt, thinking: accumulatedThinking, tools: recordedTools, timestamp: Date.now() };
1073
- if (queryNumber != null) {
1074
- queryDetails.set(queryNumber, lastQueryDetails);
1075
- if (queryDetails.size > 50) queryDetails.delete(queryDetails.keys().next().value);
1076
- }
1077
- if (responsePanelStarted) console.log('');
1078
- } catch (err) {
1079
- stopSpinner();
1080
- if (!isAbortError(err)) {
1081
- console.log(`\n ${C.red}[ERROR] ${err.message}${C.reset}\n`);
1082
- } else {
1083
- console.log(`\n ${C.amber}[INTERRUPTED] Query cancelled.${C.reset}\n`);
1084
- }
1085
- }
1086
- }
1087
1525
 
1088
1526
  async function main() {
1089
- try { process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1015l'); } catch {}
1090
-
1091
- const opts = parseCliArgs();
1527
+ const opts = parseCliArgs();
1528
+
1529
+ if (opts.version) { console.log(PACKAGE_VERSION); process.exit(0); }
1530
+ if (opts.help) { printWelcomeHero(); printHelp(); process.exit(0); }
1531
+ if (opts.showTools) {
1532
+ printWelcomeHero();
1533
+ console.log(`\n ${C.bold}${C.sakura}Tools (${toolsSchema.length})${C.reset}\n`);
1534
+ for (const [i, t] of toolsSchema.entries()) {
1535
+ console.log(` ${C.gold}${String(i + 1).padStart(2)}${C.reset} ${C.cyberCyan}${t.function.name}${C.reset} — ${C.dim}${t.function.description.slice(0, 60)}${C.reset}`);
1536
+ }
1537
+ console.log('');
1538
+ process.exit(0);
1539
+ }
1092
1540
 
1093
- if (opts.help) { printWelcomeHero(); printHelp(); process.exit(0); }
1094
- if (opts.showTools) {
1095
- printWelcomeHero();
1096
- console.log(`\n ${C.bold}${C.sakura}Tools (${toolsSchema.length})${C.reset}\n`);
1097
- for (const [i, t] of toolsSchema.entries()) {
1098
- console.log(` ${C.gold}${String(i + 1).padStart(2)}${C.reset} ${C.cyberCyan}${t.function.name}${C.reset} — ${C.dim}${t.function.description.slice(0, 60)}${C.reset}`);
1099
- }
1100
- console.log('');
1101
- process.exit(0);
1102
- }
1541
+ if (opts.showStats) { printWelcomeHero(); const r = await executeTool('get_system_stats', {}); console.log(JSON.stringify(r, null, 2)); process.exit(0); }
1542
+
1543
+ if (opts.query) {
1544
+ printWelcomeHero();
1545
+ const client = new LLMClient({
1546
+ provider: opts.provider || config.activeProvider,
1547
+ baseUrl: opts.url,
1548
+ apiKey: opts.apiKey,
1549
+ model: opts.model
1550
+ });
1551
+ console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
1552
+ const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1553
+ await executeQueryWithUI(agent, opts.query, agent.session.thinkingMode === 'show', null, 1);
1554
+ process.exit(0);
1555
+ }
1103
1556
 
1104
- if (opts.showStats) { printWelcomeHero(); const r = await executeTool('get_system_stats', {}); console.log(JSON.stringify(r, null, 2)); process.exit(0); }
1105
1557
 
1106
- if (opts.query) {
1107
- printWelcomeHero();
1108
- const client = new LLMClient({
1109
- provider: opts.provider || config.activeProvider,
1110
- baseUrl: opts.url,
1111
- apiKey: opts.apiKey,
1112
- model: opts.model
1113
- });
1114
- console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
1115
- const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1116
- await executeQueryWithUI(agent, opts.query, agent.session.thinkingMode === 'show', null, 1);
1117
- process.exit(0);
1118
- }
1558
+ try { process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1015l'); } catch { }
1559
+ enterAlternateScreen();
1119
1560
 
1561
+ const client = new LLMClient({
1562
+ provider: opts.provider || config.activeProvider,
1563
+ baseUrl: opts.url,
1564
+ apiKey: opts.apiKey,
1565
+ model: opts.model
1566
+ });
1567
+ const baseUrl = client.baseUrl;
1568
+ const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1569
+
1570
+ function cancelActiveQuery() {
1571
+ let cancelled = false;
1572
+ if (activeAbortController) {
1573
+ activeAbortController.abort();
1574
+ activeAbortController = null;
1575
+ cancelled = true;
1576
+ }
1577
+ if (agent && typeof agent.cancelResearch === 'function') {
1578
+ agent.cancelResearch();
1579
+ cancelled = true;
1580
+ }
1581
+ if (queryQueue.length > 0) {
1582
+ queryQueue.length = 0;
1583
+ cancelled = true;
1584
+ }
1585
+ if (cancelled) {
1586
+ if (globalTuiPrompt) {
1587
+ globalTuiPrompt.agentState = '';
1588
+ globalTuiPrompt.render();
1589
+ }
1590
+ return true;
1591
+ }
1592
+ return false;
1593
+ }
1120
1594
 
1121
- enterAlternateScreen();
1595
+ const tuiPrompt = new InteractiveTuiPrompt({
1596
+ prompt: 'Kiku ❯ ',
1597
+ agent,
1598
+ modelName: opts.model || client.model || 'gemma-4-e4b',
1599
+ provider: client.providerName,
1600
+ onCancel: cancelActiveQuery,
1601
+ onSigInt: async () => {
1602
+ if (activeAbortController || isProcessing) {
1603
+ cancelActiveQuery();
1604
+ } else {
1605
+ try { await agent.endSession(); } catch { }
1606
+ if (globalTuiPrompt) {
1607
+ globalTuiPrompt.dispose();
1608
+ globalTuiPrompt = null;
1609
+ }
1610
+ exitAlternateScreen();
1611
+ console.log(`\n ${C.sakura}Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
1612
+ process.exit(0);
1613
+ }
1614
+ }
1615
+ });
1122
1616
 
1123
- const client = new LLMClient({
1124
- provider: opts.provider || config.activeProvider,
1125
- baseUrl: opts.url,
1126
- apiKey: opts.apiKey,
1127
- model: opts.model
1128
- });
1129
- const baseUrl = client.baseUrl;
1130
- const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1131
-
1132
- const tuiPrompt = new InteractiveTuiPrompt({
1133
- prompt: 'Kiku ❯ ',
1134
- agent,
1135
- modelName: opts.model || client.model || 'gemma-4-e4b',
1136
- provider: client.providerName,
1137
- onSigInt: async () => {
1138
- if (activeAbortController) {
1139
- activeAbortController.abort();
1140
- activeAbortController = null;
1141
- queryQueue.length = 0;
1142
- deferredCommands.length = 0;
1143
- } else {
1144
- try { await agent.endSession(); } catch {}
1145
- if (globalTuiPrompt) {
1146
- globalTuiPrompt.dispose();
1147
- globalTuiPrompt = null;
1148
- }
1149
- exitAlternateScreen();
1150
- console.log(`\n ${C.sakura}Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
1151
- process.exit(0);
1152
- }
1153
- }
1154
- });
1617
+ globalTuiPrompt = tuiPrompt;
1618
+ setupOutputInterception(tuiPrompt, () => isProcessing);
1619
+
1620
+ let health = await client.checkConnection();
1621
+ let catalogOnlyMode = false;
1622
+
1623
+ if (!health.connected) {
1624
+ printRichCard({
1625
+ title: `${client.providerName} Offline`,
1626
+ status: 'offline',
1627
+ lines: [
1628
+ `• Endpoint: ${baseUrl || 'local'} (${health.error || 'unreachable'})`,
1629
+ ...(client.provider === 'lmstudio' ? ['• Action: Attempting auto-start of local LM Studio...'] : []),
1630
+ ],
1631
+ });
1632
+
1633
+ if (client.provider === 'lmstudio') {
1634
+ const autoHealth = await attemptAutoStartLMStudio(baseUrl, opts.model || client.model);
1635
+ if (autoHealth) {
1636
+ health = autoHealth;
1637
+ printRichCard({
1638
+ title: 'LM Studio Started',
1639
+ status: 'ok',
1640
+ lines: [
1641
+ 'Local LM Studio process started and responding.',
1642
+ ],
1643
+ });
1644
+ } else {
1645
+ catalogOnlyMode = true;
1646
+ printRichCard({
1647
+ title: 'Catalog-Only Mode',
1648
+ status: 'catalog',
1649
+ lines: [
1650
+ 'Direct database & MCP tools remain available without AI synthesis.',
1651
+ 'Start LM Studio and run /model reconnect to enable conversational chat.',
1652
+ ],
1653
+ });
1654
+ }
1655
+ } else {
1656
+ catalogOnlyMode = true;
1657
+ printRichCard({
1658
+ title: 'Catalog-Only Mode',
1659
+ status: 'catalog',
1660
+ lines: [
1661
+ 'Direct database & MCP tools remain available without AI synthesis.',
1662
+ 'Verify your provider endpoint/key and use /model reconnect.',
1663
+ ],
1664
+ });
1665
+ }
1666
+ }
1155
1667
 
1156
- globalTuiPrompt = tuiPrompt;
1157
- setupOutputInterception(tuiPrompt, () => isProcessing);
1668
+ const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1669
+ const modelResolution = catalogOnlyMode
1670
+ ? { connected: false, state: 'catalog_only' }
1671
+ : await modelManager.resolve({ requestedModel: opts.model || client.model });
1672
+ if (!catalogOnlyMode && (!modelResolution.connected || !modelResolution.selectedModel)) {
1673
+ printRichCard({
1674
+ title: 'Model Not Loaded',
1675
+ status: 'warning',
1676
+ lines: [
1677
+ modelResolution.error || 'No chat-capable model loaded',
1678
+ 'Load a model in LM Studio, then type /model reconnect.',
1679
+ ],
1680
+ });
1681
+ catalogOnlyMode = true;
1682
+ } else if (!catalogOnlyMode && modelResolution.selectedModel) {
1683
+ tuiPrompt.modelName = modelResolution.selectedModel;
1684
+ printRichCard({
1685
+ title: 'Connected & Ready',
1686
+ status: 'connected',
1687
+ lines: [
1688
+ `• Provider: ${client.providerName} (Local) • Model: ${modelResolution.selectedModel}`,
1689
+ `• Status: Chat & Tool DAG Active • Tools: ${toolsSchema.length} Loaded`,
1690
+ ],
1691
+ });
1692
+ }
1693
+ if (modelResolution.usedFallback) {
1694
+ printRichCard({
1695
+ title: 'Model Fallback',
1696
+ status: 'warning',
1697
+ lines: [
1698
+ `Requested model ${opts.model} was unavailable.`,
1699
+ `Fallback model ${modelResolution.selectedModel} selected.`,
1700
+ ],
1701
+ });
1702
+ }
1158
1703
 
1159
- let health = await client.checkConnection();
1160
- let catalogOnlyMode = false;
1704
+ if (opts.mcp) {
1705
+ try {
1706
+ if (opts.mcp.endsWith('.json')) {
1707
+ await agent.mcpManager.loadConfigFile(opts.mcp);
1708
+ printRichCard({
1709
+ title: 'MCP Configuration',
1710
+ status: 'mcp',
1711
+ lines: [`Loaded MCP configuration from ${opts.mcp}`],
1712
+ });
1713
+ } else {
1714
+ const res = await agent.connectMcp('remote', { url: opts.mcp });
1715
+ printRichCard({
1716
+ title: 'MCP Connected',
1717
+ status: 'mcp',
1718
+ lines: [
1719
+ `Server: ${opts.mcp}`,
1720
+ `Tools registered: ${res.toolsCount}`,
1721
+ ],
1722
+ });
1723
+ }
1724
+ } catch (err) {
1725
+ printRichCard({
1726
+ title: 'MCP Error',
1727
+ status: 'error',
1728
+ lines: [err.message],
1729
+ });
1730
+ }
1731
+ }
1161
1732
 
1162
- if (!health.connected) {
1163
- console.log(` ${C.amber}!! ${client.providerName} offline or unreachable${C.reset}`);
1164
- console.log(` ${C.dim}• ${health.error}${C.reset}\n`);
1733
+ await agent._ready;
1734
+
1735
+ const mcpStatus = agent.mcpManager ? agent.mcpManager.getStatus() : null;
1736
+ if (mcpStatus && mcpStatus.activeServersCount > 0 && !opts.mcp) {
1737
+ const serverSummaries = mcpStatus.servers
1738
+ .filter(s => s.status === 'connected')
1739
+ .map(s => `• ${s.name} (${s.type}) — ${s.toolsCount} tools`);
1740
+ printRichCard({
1741
+ title: 'Persisted MCP Connected',
1742
+ status: 'mcp',
1743
+ lines: [
1744
+ `Reconnected ${mcpStatus.activeServersCount} saved MCP server(s):`,
1745
+ ...serverSummaries,
1746
+ ],
1747
+ });
1748
+ }
1165
1749
 
1166
- if (client.provider === 'lmstudio') {
1167
- console.log(` ${C.bold}${C.sakura}Attempting auto-start...${C.reset}`);
1168
- const autoHealth = await attemptAutoStartLMStudio(baseUrl, opts.model || client.model);
1169
- if (autoHealth) {
1170
- health = autoHealth;
1171
- console.log(` ${C.emerald}[OK] LM Studio started.${C.reset}\n`);
1172
- } else {
1173
- catalogOnlyMode = true;
1174
- console.log(` ${C.amber}[CATALOG-ONLY] Kiku will remain available without AI synthesis.${C.reset}\n`);
1175
- }
1176
- } else {
1177
- catalogOnlyMode = true;
1178
- console.log(` ${C.amber}[CATALOG-ONLY] Kiku will remain available without AI synthesis.${C.reset}\n`);
1179
- }
1180
- }
1750
+ if (agent.restoredMessageCount > 0) {
1751
+ const s = agent.session;
1752
+ console.log(`\n ${C.sakura}${C.bold}[SESSION RESTORED]${C.reset} ${C.dim}${s.id} — ${s.messages.length} messages${C.reset}\n`);
1753
+ }
1181
1754
 
1182
- const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1183
- const modelResolution = catalogOnlyMode
1184
- ? { connected: false, state: 'catalog_only' }
1185
- : await modelManager.resolve({ requestedModel: opts.model || client.model });
1186
- if (!catalogOnlyMode && (!modelResolution.connected || !modelResolution.selectedModel)) {
1187
- console.log(` ${C.amber}[MODEL WARNING] ${modelResolution.error}${C.reset}`);
1188
- catalogOnlyMode = true;
1189
- console.log(` ${C.amber}[CATALOG-ONLY] Load or select a chat model, then use /model reconnect.${C.reset}\n`);
1190
- }
1191
- if (modelResolution.selectedModel) {
1192
- tuiPrompt.modelName = modelResolution.selectedModel;
1193
- }
1194
- if (modelResolution.usedFallback) {
1195
- console.log(` ${C.amber}[MODEL FALLBACK] Requested ${opts.model}, using ${modelResolution.selectedModel}.${C.reset}\n`);
1196
- }
1755
+ agent.stateMachine.on('enter:' + AgentStates.FETCHING_TOOL, ({ toolName }) => {
1756
+ if (globalTuiPrompt) {
1757
+ globalTuiPrompt.agentState = `Tool: ${toolName}`;
1758
+ globalTuiPrompt.agentStateTime = Date.now();
1759
+ globalTuiPrompt.render();
1760
+ }
1761
+ });
1197
1762
 
1198
- if (opts.mcp) {
1199
- try {
1200
- if (opts.mcp.endsWith('.json')) {
1201
- await agent.mcpManager.loadConfigFile(opts.mcp);
1202
- console.log(` ${C.emerald}[OK] Loaded MCP configuration from ${opts.mcp}${C.reset}\n`);
1203
- } else {
1204
- const res = await agent.connectMcp('remote', { url: opts.mcp });
1205
- console.log(` ${C.emerald}[OK] MCP Connected: ${opts.mcp} (${res.toolsCount} Tools)${C.reset}\n`);
1206
- }
1207
- } catch (err) {
1208
- console.log(` ${C.red}[MCP Error] ${err.message}${C.reset}\n`);
1209
- }
1210
- }
1763
+ agent.stateMachine.on('enter:' + AgentStates.STREAMING_THOUGHTS, () => {
1764
+ if (globalTuiPrompt) {
1765
+ globalTuiPrompt.agentState = 'Thinking…';
1766
+ globalTuiPrompt.agentStateTime = Date.now();
1767
+ globalTuiPrompt.render();
1768
+ }
1769
+ });
1211
1770
 
1212
- await agent._ready;
1771
+ agent.stateMachine.on('enter:' + AgentStates.STREAMING_ANSWER, () => {
1772
+ if (globalTuiPrompt) {
1773
+ globalTuiPrompt.agentState = 'Streaming…';
1774
+ globalTuiPrompt.agentStateTime = Date.now();
1775
+ globalTuiPrompt.render();
1776
+ }
1777
+ });
1213
1778
 
1214
- if (agent.restoredMessageCount > 0) {
1215
- const s = agent.session;
1216
- console.log(`\n ${C.sakura}${C.bold}[SESSION RESTORED]${C.reset} ${C.dim}${s.id} — ${s.messages.length} messages${C.reset}\n`);
1217
- }
1779
+ agent.stateMachine.on('enter:' + AgentStates.IDLE, () => {
1780
+ if (globalTuiPrompt) {
1781
+ globalTuiPrompt.agentState = '';
1782
+ }
1783
+ });
1218
1784
 
1219
- agent.stateMachine.on('enter:' + AgentStates.FETCHING_TOOL, ({ toolName }) => {
1220
- if (globalTuiPrompt) {
1221
- globalTuiPrompt.agentState = `Tool: ${toolName}`;
1222
- globalTuiPrompt.agentStateTime = Date.now();
1223
- }
1224
- });
1785
+ const cmdCtx = {
1786
+ tuiPrompt,
1787
+ agent,
1788
+ client,
1789
+ baseUrl,
1790
+ modelManager,
1791
+ catalogOnlyMode,
1792
+ setCatalogOnlyMode: value => { catalogOnlyMode = Boolean(value); },
1793
+ get lastQuery() { return lastQuery; },
1794
+ set lastQuery(value) { lastQuery = value; },
1795
+ printHelp,
1796
+ printQueryDetails,
1797
+ copyQueryDetails,
1798
+ printWelcome: printWelcomeHero,
1799
+ printAd,
1800
+ cancelActiveQuery,
1801
+ enqueueQuery: async (prompt, isRetry) => {
1802
+ queryQueue.push({ id: 'q_' + Date.now(), number: nextQueryNumber++, prompt, addedAt: Date.now() });
1803
+ triggerQueueProcessing();
1804
+ },
1805
+ clearQueryQueue: () => { queryQueue.length = 0; },
1806
+ getQueryQueueLength: () => queryQueue.length,
1807
+ get isProcessing() { return isProcessing; },
1808
+ setIsProcessing: (val, label = '') => {
1809
+ isProcessing = Boolean(val);
1810
+ if (globalTuiPrompt) {
1811
+ globalTuiPrompt.agentState = label || '';
1812
+ if (label) globalTuiPrompt.agentStateTime = Date.now();
1813
+ globalTuiPrompt.render();
1814
+ }
1815
+ },
1816
+ setAgentState: (label, time = Date.now()) => {
1817
+ if (globalTuiPrompt) {
1818
+ globalTuiPrompt.agentState = label || '';
1819
+ globalTuiPrompt.agentStateTime = time;
1820
+ globalTuiPrompt.render();
1821
+ }
1822
+ },
1823
+ };
1824
+
1825
+ const triggerQueueProcessing = () => {
1826
+ if (isProcessing || queryQueue.length === 0) return;
1827
+ isProcessing = true;
1828
+
1829
+ (async () => {
1830
+ try {
1831
+ while (queryQueue.length > 0) {
1832
+ const nextItem = queryQueue.shift();
1833
+ activeQueryNumber = nextItem.number;
1834
+ cmdCtx.lastQuery = nextItem.prompt;
1835
+ activeAbortController = new AbortController();
1836
+
1837
+ try {
1838
+ if (catalogOnlyMode) {
1839
+ const targetTool = nextItem.prompt.startsWith('@')
1840
+ ? nextItem.prompt.slice(1).split(/\s+/)[0]
1841
+ : 'search_works';
1842
+ const rawArg = nextItem.prompt.startsWith('@')
1843
+ ? nextItem.prompt.slice(targetTool.length + 1).trim()
1844
+ : nextItem.prompt;
1845
+
1846
+ let queryArgs = {};
1847
+ try { queryArgs = JSON.parse(rawArg); } catch { queryArgs = { query: rawArg }; }
1848
+
1849
+ let result;
1850
+ if (agent.mcpManager?.isMcpTool(targetTool)) {
1851
+ result = await agent.mcpManager.callTool(targetTool, queryArgs);
1852
+ } else {
1853
+ result = await executeTool(targetTool, queryArgs);
1854
+ }
1855
+
1856
+ const serialized = JSON.stringify(result, null, 2);
1857
+ printSolidPanel({
1858
+ title: `Direct Tool Result • ${targetTool} • Q${nextItem.number}`,
1859
+ lines: serialized.split('\n'),
1860
+ });
1861
+ } else {
1862
+ await executeQueryWithUI(agent, nextItem.prompt, agent.session.thinkingMode === 'show', activeAbortController.signal, nextItem.number);
1863
+ }
1864
+ } catch (err) {
1865
+ if (!isAbortError(err)) {
1866
+ printRichCard({
1867
+ title: 'Execution Error',
1868
+ status: 'error',
1869
+ lines: [err.message],
1870
+ });
1871
+ }
1872
+ } finally {
1873
+ activeAbortController = null;
1874
+ queryCount++;
1875
+ printAd();
1876
+ activeQueryNumber = null;
1877
+ if (globalTuiPrompt) {
1878
+ globalTuiPrompt.agentState = '';
1879
+ globalTuiPrompt.render();
1880
+ }
1881
+ }
1882
+ }
1883
+ } finally {
1884
+ isProcessing = false;
1885
+ if (globalTuiPrompt) {
1886
+ globalTuiPrompt.agentState = '';
1887
+ globalTuiPrompt.render();
1888
+ }
1889
+ }
1890
+ })();
1891
+ };
1892
+
1893
+ while (true) {
1894
+ let rawInput;
1895
+
1896
+ if (deferredCommands.length > 0 && !isProcessing) {
1897
+ rawInput = deferredCommands.shift();
1898
+ printRichCard({
1899
+ title: 'Deferred Command Executed',
1900
+ status: 'info',
1901
+ lines: [rawInput],
1902
+ });
1903
+ } else {
1904
+ rawInput = await tuiPrompt.readLine();
1905
+ }
1225
1906
 
1226
- agent.stateMachine.on('enter:' + AgentStates.STREAMING_THOUGHTS, () => {
1227
- if (globalTuiPrompt) {
1228
- globalTuiPrompt.agentState = 'Thinking…';
1229
- globalTuiPrompt.agentStateTime = Date.now();
1230
- }
1231
- });
1907
+ if (rawInput === null) break;
1908
+ const trimmed = rawInput.trim();
1909
+ if (!trimmed) continue;
1232
1910
 
1233
- agent.stateMachine.on('enter:' + AgentStates.STREAMING_ANSWER, () => {
1234
- if (globalTuiPrompt) {
1235
- globalTuiPrompt.agentState = 'Streaming…';
1236
- globalTuiPrompt.agentStateTime = Date.now();
1237
- }
1238
- });
1911
+ if (trimmed.startsWith('/')) {
1912
+ const lower = trimmed.toLowerCase();
1913
+ if (lower === '/cancel' || lower === '/abort' || (lower === '/stop' && isProcessing)) {
1914
+ cancelActiveQuery();
1915
+ continue;
1916
+ }
1239
1917
 
1240
- agent.stateMachine.on('enter:' + AgentStates.IDLE, () => {
1241
- if (globalTuiPrompt) {
1242
- globalTuiPrompt.agentState = '';
1243
- }
1244
- });
1918
+ if (isProcessing) {
1919
+ deferredCommands.push(trimmed);
1920
+ printRichCard({
1921
+ title: 'Command Queued',
1922
+ status: 'info',
1923
+ lines: [`• "${trimmed}" queued to run when active processing finishes.`],
1924
+ });
1925
+ continue;
1926
+ }
1245
1927
 
1246
- async function processNextQueueItem() {
1247
- if (isProcessing || queryQueue.length === 0) return;
1928
+ const parts = trimmed.split(/\s+/);
1929
+ const sub = parts[1] || '';
1930
+ const extra = parts.slice(2).join(' ');
1931
+
1932
+ let result;
1933
+ try {
1934
+ result = await routeCommand(trimmed, {
1935
+ ...cmdCtx,
1936
+ sub,
1937
+ extra,
1938
+ parts,
1939
+ });
1940
+ } catch (cmdErr) {
1941
+ printRichCard({
1942
+ title: 'Command Error',
1943
+ status: 'error',
1944
+ lines: [cmdErr.message || String(cmdErr)],
1945
+ });
1946
+ continue;
1947
+ }
1248
1948
 
1249
- isProcessing = true;
1250
- while (queryQueue.length > 0) {
1251
- const nextItem = queryQueue.shift();
1252
- lastQuery = nextItem.prompt;
1253
- activeQueryNumber = nextItem.number;
1949
+ if (result === 'EXIT') {
1950
+ cancelActiveQuery();
1951
+ break;
1952
+ }
1953
+ if (result === 'RETRY' && cmdCtx.lastQuery) {
1954
+ const retryNumber = nextQueryNumber++;
1955
+ printSolidPanel({
1956
+ title: `${C.roseGlow}${C.bold}Question ${retryNumber} • Retry${C.reset}`,
1957
+ lines: wrapText(cmdCtx.lastQuery, getFullWidth() - 8)
1958
+ .map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
1959
+ bg: C.bgPanel
1960
+ });
1961
+ queryQueue.push({ id: 'q_' + Date.now(), number: retryNumber, prompt: cmdCtx.lastQuery, addedAt: Date.now() });
1962
+ triggerQueueProcessing();
1963
+ }
1964
+ continue;
1965
+ }
1254
1966
 
1255
- activeAbortController = new AbortController();
1256
- try {
1257
- if (catalogOnlyMode) {
1258
- let targetTool = 'global_search';
1259
- let queryArgs = { query: nextItem.prompt };
1260
- const atMatch = nextItem.prompt.match(/^@([a-zA-Z0-9_-]+)(?:\s+(.*))?$/);
1261
- if (atMatch) {
1262
- targetTool = atMatch[1];
1263
- const rawArg = (atMatch[2] || '').trim();
1264
- if (rawArg) {
1265
- try { queryArgs = JSON.parse(rawArg); } catch { queryArgs = { query: rawArg }; }
1266
- } else {
1267
- queryArgs = {};
1967
+ const { sanitized, warnings } = sanitizeQuery(trimmed);
1968
+ if (warnings.length > 0) {
1969
+ printRichCard({
1970
+ title: 'Query Warning',
1971
+ status: 'warning',
1972
+ lines: warnings,
1973
+ });
1268
1974
  }
1269
- }
1270
1975
 
1271
- let result;
1272
- if (agent.mcpManager?.isMcpTool(targetTool)) {
1273
- result = await agent.mcpManager.callTool(targetTool, queryArgs);
1274
- } else {
1275
- result = await executeTool(targetTool, queryArgs);
1976
+ const queryNumber = nextQueryNumber++;
1977
+ const queuePosition = isProcessing ? queryQueue.length + 1 : 0;
1978
+ const queuedLabel = queuePosition > 0 ? ` • Queued ${queuePosition}` : '';
1979
+ printSolidPanel({
1980
+ title: `${C.roseGlow}${C.bold}Question ${queryNumber} • ${randomFanName()}${queuedLabel}${C.reset}`,
1981
+ lines: wrapText(trimmed, getFullWidth() - 8).map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
1982
+ bg: C.bgPanel
1983
+ });
1984
+
1985
+ lastQuery = trimmed;
1986
+ queryQueue.push({ id: 'q_' + Date.now(), number: queryNumber, prompt: trimmed, addedAt: Date.now() });
1987
+ triggerQueueProcessing();
1276
1988
  }
1277
1989
 
1278
- const serialized = JSON.stringify(result, null, 2);
1279
- printSolidPanel({
1280
- title: `Direct Tool Result • ${targetTool} • Q${nextItem.number}`,
1281
- lines: [
1282
- ...serialized.slice(0, 6000).split('\n'),
1283
- ...(serialized.length > 6000 ? [`${C.dim}… result truncated${C.reset}`] : []),
1284
- ],
1285
- });
1286
- } else {
1287
- await executeQueryWithUI(agent, nextItem.prompt, agent.session.thinkingMode === 'show', activeAbortController.signal, nextItem.number);
1990
+ if (globalTuiPrompt) {
1991
+ globalTuiPrompt.dispose();
1992
+ globalTuiPrompt = null;
1993
+ }
1994
+ exitAlternateScreen();
1995
+ console.log(`\n ${C.sakura}Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
1996
+ process.exit(0);
1288
1997
  }
1289
- } finally {
1290
- activeAbortController = null;
1291
- queryCount++;
1292
- printAd();
1293
- activeQueryNumber = null;
1294
- }
1295
- }
1296
- isProcessing = false;
1297
- }
1298
-
1299
- while (true) {
1300
- let rawInput;
1301
-
1302
- if (deferredCommands.length > 0 && !isProcessing) {
1303
- rawInput = deferredCommands.shift();
1304
- console.log(`\n ${C.cyberCyan}[DEFERRED] ${rawInput}${C.reset}\n`);
1305
- } else {
1306
- rawInput = await tuiPrompt.readLine();
1307
- }
1308
-
1309
- if (rawInput === null) break;
1310
- const trimmed = rawInput.trim();
1311
- if (!trimmed) continue;
1312
-
1313
- const queryNumber = !trimmed.startsWith('/') ? nextQueryNumber++ : null;
1314
1998
 
1315
- if (!trimmed.startsWith('/')) {
1316
- const wrapped = wrapText(trimmed, getFullWidth() - 8);
1317
- const queuePosition = isProcessing ? queryQueue.length + 1 : 0;
1318
- const queuedLabel = queuePosition > 0 ? ` • Queued ${queuePosition}` : '';
1319
- printSolidPanel({
1320
- title: `${C.roseGlow}${C.bold}Question ${queryNumber} • ${randomFanName()}${queuedLabel}${C.reset}`,
1321
- lines: wrapped.map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
1322
- bg: '\x1b[48;2;36;40;52m'
1323
- });
1324
- }
1325
-
1326
- if (trimmed.startsWith('/')) {
1327
- if (isProcessing) {
1328
- deferredCommands.push(trimmed);
1329
- console.log(`\n ${C.cyberCyan}[DEFERRED] ${trimmed}${C.reset}\n`);
1330
- tuiPrompt.render();
1331
- continue;
1332
- }
1333
-
1334
- const cmdCtx = {
1335
- tuiPrompt,
1336
- agent,
1337
- client,
1338
- baseUrl,
1339
- modelManager,
1340
- catalogOnlyMode,
1341
- setCatalogOnlyMode: value => { catalogOnlyMode = Boolean(value); },
1342
- sub: trimmed.split(/\s+/)[1] || '',
1343
- extra: trimmed.split(/\s+/).slice(2).join(' '),
1344
- parts: trimmed.split(/\s+/),
1345
- lastQuery,
1346
- printHelp,
1347
- printQueryDetails,
1348
- printWelcome: printWelcomeHero,
1349
- printAd,
1350
- enqueueQuery: async (prompt, isRetry) => {
1351
- queryQueue.push({ id: 'q_' + Date.now(), number: nextQueryNumber++, prompt, addedAt: Date.now() });
1352
- await processNextQueueItem();
1353
- },
1354
- clearQueryQueue: () => { queryQueue.length = 0; },
1355
- getQueryQueueLength: () => queryQueue.length,
1356
- };
1357
-
1358
- const result = await routeCommand(trimmed, cmdCtx);
1359
-
1360
- if (result === 'EXIT') break;
1361
- if (result === 'RETRY' && cmdCtx.lastQuery) {
1362
- const retryNumber = nextQueryNumber++;
1363
- printSolidPanel({
1364
- title: `${C.roseGlow}${C.bold}Question ${retryNumber} • Retry${C.reset}`,
1365
- lines: wrapText(cmdCtx.lastQuery, getFullWidth() - 8)
1366
- .map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
1367
- bg: '\x1b[48;2;36;40;52m'
1999
+ process.on('exit', () => {
2000
+ try { globalTuiPrompt?.dispose(); } catch { }
2001
+ exitAlternateScreen();
1368
2002
  });
1369
- queryQueue.push({ id: 'q_' + Date.now(), number: retryNumber, prompt: cmdCtx.lastQuery, addedAt: Date.now() });
1370
- await processNextQueueItem();
1371
- }
1372
- continue;
1373
- }
1374
-
1375
- const { sanitized, warnings } = sanitizeQuery(trimmed);
1376
- if (warnings.length > 0) {
1377
- console.log(`\n ${C.amber}[WARNING] ${warnings.join('; ')}${C.reset}\n`);
1378
- }
1379
-
1380
- lastQuery = trimmed;
1381
- queryQueue.push({ id: 'q_' + Date.now(), number: queryNumber, prompt: trimmed, addedAt: Date.now() });
1382
- await processNextQueueItem();
1383
- }
1384
-
1385
- if (globalTuiPrompt) {
1386
- globalTuiPrompt.dispose();
1387
- globalTuiPrompt = null;
1388
- }
1389
- exitAlternateScreen();
1390
- console.log(`\n ${C.sakura}Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
1391
- process.exit(0);
1392
- }
2003
+ process.on('uncaughtException', (err) => {
2004
+ try { globalTuiPrompt?.dispose(); } catch { }
2005
+ exitAlternateScreen();
2006
+ console.error(`\n Uncaught: ${err.message}`);
2007
+ process.exit(1);
2008
+ });
2009
+ process.on('SIGINT', () => {
2010
+ try { globalTuiPrompt?.dispose(); } catch { }
2011
+ exitAlternateScreen();
2012
+ process.exit(130);
2013
+ });
1393
2014
 
1394
- process.on('exit', () => {
1395
- try { globalTuiPrompt?.dispose(); } catch {}
1396
- exitAlternateScreen();
1397
- });
1398
- process.on('uncaughtException', (err) => {
1399
- try { globalTuiPrompt?.dispose(); } catch {}
1400
- exitAlternateScreen();
1401
- console.error(`\n Uncaught: ${err.message}`);
1402
- process.exit(1);
1403
- });
1404
- process.on('SIGINT', () => {
1405
- try { globalTuiPrompt?.dispose(); } catch {}
1406
- exitAlternateScreen();
1407
- process.exit(130);
1408
- });
1409
-
1410
- main().catch(err => {
1411
- exitAlternateScreen();
1412
- console.error(`\n ${C.red}Fatal: ${err.message}${C.reset}\n`);
1413
- process.exit(1);
1414
- });
2015
+ main().catch(err => {
2016
+ exitAlternateScreen();
2017
+ console.error(`\n ${C.red}Fatal: ${err.message}${C.reset}\n`);
2018
+ process.exit(1);
2019
+ });