kiku-agent 1.0.5 → 1.0.7

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