@aitherium/shell-cli 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +68 -0
- package/dist/auth.js +288 -0
- package/dist/client.d.ts +116 -0
- package/dist/client.js +528 -0
- package/dist/command-registry.d.ts +71 -0
- package/dist/command-registry.js +223 -0
- package/dist/commands.d.ts +14 -0
- package/dist/commands.js +6785 -0
- package/dist/completions.d.ts +27 -0
- package/dist/completions.js +351 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +48 -0
- package/dist/gargbot.d.ts +11 -0
- package/dist/gargbot.js +230 -0
- package/dist/jobs.d.ts +65 -0
- package/dist/jobs.js +386 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +389 -0
- package/dist/notebooks.d.ts +19 -0
- package/dist/notebooks.js +685 -0
- package/dist/products.d.ts +12 -0
- package/dist/products.js +159 -0
- package/dist/renderer.d.ts +104 -0
- package/dist/renderer.js +1812 -0
- package/dist/repl.d.ts +16 -0
- package/dist/repl.js +1190 -0
- package/dist/session-store.d.ts +35 -0
- package/dist/session-store.js +153 -0
- package/dist/tunnel.d.ts +13 -0
- package/dist/tunnel.js +169 -0
- package/package.json +36 -0
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,1812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal renderer — banners, streaming tokens, markdown, tables.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { marked } from 'marked';
|
|
7
|
+
import { markedTerminal } from 'marked-terminal';
|
|
8
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
// Configure marked for terminal rendering
|
|
12
|
+
marked.use(markedTerminal({ reflowText: true, width: 80 }));
|
|
13
|
+
/* ── Banner ─────────────────────────────────────────────────── */
|
|
14
|
+
export function renderBanner(info) {
|
|
15
|
+
const version = 'v1.1.0';
|
|
16
|
+
const title = `AitherShell ${version}`;
|
|
17
|
+
let connected;
|
|
18
|
+
if (info.genesisOnline === true) {
|
|
19
|
+
const label = info.backendName || (info.backendType === 'adk' ? 'agent' : 'Genesis');
|
|
20
|
+
connected = `Connected to ${label} (${info.genesis})`;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
connected = `Offline (${info.genesis})`;
|
|
24
|
+
}
|
|
25
|
+
// Build service status line from direct probes
|
|
26
|
+
const probed = info.serviceLines || [];
|
|
27
|
+
const upNames = probed.filter(s => s.up).map(s => s.name);
|
|
28
|
+
const downNames = probed.filter(s => !s.up).map(s => s.name);
|
|
29
|
+
const upCount = upNames.length;
|
|
30
|
+
const parts = [
|
|
31
|
+
info.services != null ? `${info.services} services` : (upCount > 0 ? `${upCount} services` : null),
|
|
32
|
+
info.agents != null ? `${info.agents} agents` : null,
|
|
33
|
+
info.llm ? info.llm : null,
|
|
34
|
+
].filter(Boolean);
|
|
35
|
+
const stats = parts.join(' \u00b7 ') || 'no services detected';
|
|
36
|
+
const userLine = info.user ? `Logged in as ${info.user}` : '';
|
|
37
|
+
// Build the service detail lines: "up: vLLM, ComfyUI, ..." "down: Watch, ..."
|
|
38
|
+
const detailLines = [];
|
|
39
|
+
if (upNames.length > 0)
|
|
40
|
+
detailLines.push(`UP: ${upNames.join(', ')}`);
|
|
41
|
+
if (downNames.length > 0)
|
|
42
|
+
detailLines.push(`DN: ${downNames.join(', ')}`);
|
|
43
|
+
const allLines = [connected, stats, ...detailLines, userLine].filter(Boolean);
|
|
44
|
+
const contentWidth = Math.max(...allLines.map(l => l.length));
|
|
45
|
+
const inner = contentWidth + 2; // 1 space padding each side
|
|
46
|
+
const titleBar = `\u2500 ${title} `;
|
|
47
|
+
const topPad = Math.max(0, inner - titleBar.length);
|
|
48
|
+
const border = chalk.cyan;
|
|
49
|
+
const pad = (raw, colored) => {
|
|
50
|
+
const display = colored || raw;
|
|
51
|
+
const gap = Math.max(0, contentWidth - raw.length);
|
|
52
|
+
return border(` \u2502`) + ` ${display}${' '.repeat(gap)} ` + border(`\u2502`);
|
|
53
|
+
};
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(border(` \u256d${titleBar}${'\u2500'.repeat(topPad)}\u256e`));
|
|
56
|
+
const connColored = info.genesisOnline === true ? chalk.green(connected) : chalk.red(connected);
|
|
57
|
+
console.log(pad(connected, connColored));
|
|
58
|
+
console.log(pad(stats));
|
|
59
|
+
if (upNames.length > 0) {
|
|
60
|
+
const upLine = `UP: ${upNames.join(', ')}`;
|
|
61
|
+
console.log(pad(upLine, chalk.green(upLine)));
|
|
62
|
+
}
|
|
63
|
+
if (downNames.length > 0) {
|
|
64
|
+
const dnLine = `DN: ${downNames.join(', ')}`;
|
|
65
|
+
console.log(pad(dnLine, chalk.red(dnLine)));
|
|
66
|
+
}
|
|
67
|
+
if (userLine) {
|
|
68
|
+
console.log(pad(userLine, chalk.green(userLine)));
|
|
69
|
+
}
|
|
70
|
+
console.log(border(` \u2570${'\u2500'.repeat(inner)}\u256f`));
|
|
71
|
+
console.log();
|
|
72
|
+
}
|
|
73
|
+
/* ── Output cleanup ────────────────────────────────────────── */
|
|
74
|
+
/** Decode HTML entities that LLMs dump from scraped web content. */
|
|
75
|
+
function decodeHtmlEntities(text) {
|
|
76
|
+
return text
|
|
77
|
+
.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(Number(code)))
|
|
78
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
79
|
+
.replace(/&/g, '&')
|
|
80
|
+
.replace(/</g, '<')
|
|
81
|
+
.replace(/>/g, '>')
|
|
82
|
+
.replace(/"/g, '"')
|
|
83
|
+
.replace(/'/g, "'")
|
|
84
|
+
.replace(/–/g, '–')
|
|
85
|
+
.replace(/—/g, '—')
|
|
86
|
+
.replace(/‘/g, '\u2018')
|
|
87
|
+
.replace(/’/g, '\u2019')
|
|
88
|
+
.replace(/“/g, '\u201C')
|
|
89
|
+
.replace(/”/g, '\u201D')
|
|
90
|
+
.replace(/&/g, '&')
|
|
91
|
+
.replace(/'/g, "'");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Strip website navigation chrome, cookie banners, menu dumps, and other
|
|
95
|
+
* non-content noise that LLMs paste verbatim from scraped pages.
|
|
96
|
+
*/
|
|
97
|
+
function stripWebCruft(text) {
|
|
98
|
+
const lines = text.split('\n');
|
|
99
|
+
const cleaned = [];
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
// Skip lines that are mostly bullet-separated nav items (5+ bullets)
|
|
103
|
+
if ((trimmed.match(/ • /g) || []).length >= 4)
|
|
104
|
+
continue;
|
|
105
|
+
// Skip lines that are just "Primary Menu Sections" / nav headers
|
|
106
|
+
if (/^(Primary|Main|Footer|Secondary)\s+Menu/i.test(trimmed))
|
|
107
|
+
continue;
|
|
108
|
+
// Skip cookie/consent boilerplate
|
|
109
|
+
if (/^(We use cookies|Accept all|Cookie settings|Privacy Policy$)/i.test(trimmed))
|
|
110
|
+
continue;
|
|
111
|
+
// Skip lines that are just a chain of pipe-separated short items (nav)
|
|
112
|
+
if (/^(\s*\|?\s*\w[\w\s]{1,20}\s*\|){4,}/.test(trimmed))
|
|
113
|
+
continue;
|
|
114
|
+
cleaned.push(line);
|
|
115
|
+
}
|
|
116
|
+
return cleaned.join('\n');
|
|
117
|
+
}
|
|
118
|
+
/* ── Markdown ───────────────────────────────────────────────── */
|
|
119
|
+
/**
|
|
120
|
+
* Resolve `/api/files?path=...` image references to real file paths
|
|
121
|
+
* and render as clickable OSC 8 terminal hyperlinks.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveImagePath(relativePath) {
|
|
124
|
+
const { homedir } = require('os');
|
|
125
|
+
const { resolve } = require('path');
|
|
126
|
+
const home = homedir();
|
|
127
|
+
// Try common AitherOS paths
|
|
128
|
+
const candidates = [
|
|
129
|
+
resolve(home, 'AitherOS', relativePath),
|
|
130
|
+
resolve(home, 'AitherOS-Fresh', 'AitherOS', relativePath),
|
|
131
|
+
resolve('D:', 'AitherOS-Fresh', 'AitherOS', relativePath),
|
|
132
|
+
resolve('/app', 'AitherOS', relativePath),
|
|
133
|
+
];
|
|
134
|
+
for (const p of candidates) {
|
|
135
|
+
try {
|
|
136
|
+
if (require('fs').existsSync(p))
|
|
137
|
+
return p;
|
|
138
|
+
}
|
|
139
|
+
catch { }
|
|
140
|
+
}
|
|
141
|
+
return resolve(relativePath);
|
|
142
|
+
}
|
|
143
|
+
function resolveImagePaths(text) {
|
|
144
|
+
// Match  or 
|
|
145
|
+
return text.replace(/!\[([^\]]*)\]\((?:\/api\/files\?path=)?([^)]+)\)/g, (_match, alt, rawPath) => {
|
|
146
|
+
// Resolve to absolute path — Library is under AitherOS/
|
|
147
|
+
let absPath = rawPath;
|
|
148
|
+
if (!rawPath.match(/^[A-Za-z]:[/\\]/) && !rawPath.startsWith('/')) {
|
|
149
|
+
// Relative path — resolve from repo root
|
|
150
|
+
const repoRoot = process.env.AITHER_ROOT || process.cwd();
|
|
151
|
+
absPath = `${repoRoot}/AitherOS/${rawPath}`.replace(/\//g, '\\');
|
|
152
|
+
}
|
|
153
|
+
const label = alt || rawPath.split(/[/\\]/).pop() || 'image';
|
|
154
|
+
const fileUrl = `file:///${absPath.replace(/\\/g, '/')}`;
|
|
155
|
+
// OSC 8 terminal hyperlink: \e]8;;url\e\\ text \e]8;;\e\\
|
|
156
|
+
const link = `\x1b]8;;${fileUrl}\x1b\\${chalk.cyan.underline(label)}\x1b]8;;\x1b\\`;
|
|
157
|
+
return `${link} ${chalk.dim(absPath)}`;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
export function renderMarkdown(text) {
|
|
161
|
+
try {
|
|
162
|
+
let clean = text;
|
|
163
|
+
clean = decodeHtmlEntities(clean);
|
|
164
|
+
clean = stripWebCruft(clean);
|
|
165
|
+
clean = resolveImagePaths(clean);
|
|
166
|
+
return marked.parse(clean);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return text;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/* ── Bare Code Wrapping ────────────────────────────────────── */
|
|
173
|
+
/**
|
|
174
|
+
* Detect unformatted code in text and wrap it in markdown fences.
|
|
175
|
+
* Safety net for when the LLM ignores the prompt and dumps bare code.
|
|
176
|
+
*/
|
|
177
|
+
function wrapBareCode(text) {
|
|
178
|
+
const lines = text.split('\n');
|
|
179
|
+
const result = [];
|
|
180
|
+
let codeBlock = [];
|
|
181
|
+
let inCode = false;
|
|
182
|
+
let inFence = false; // Track existing fenced regions to skip them
|
|
183
|
+
const CODE_PATTERNS = /^(def |class |import |from |async def |export |const |let |var |function |if __name__|@\w+| {2,}\S)/;
|
|
184
|
+
function flushCode() {
|
|
185
|
+
if (codeBlock.length >= 3) {
|
|
186
|
+
// Guess language from content
|
|
187
|
+
const joined = codeBlock.join('\n');
|
|
188
|
+
const lang = /\b(def |import |from |async def |if __name__)/.test(joined) ? 'python'
|
|
189
|
+
: /\b(const |let |var |function |export |=>)/.test(joined) ? 'javascript'
|
|
190
|
+
: 'text';
|
|
191
|
+
result.push('```' + lang);
|
|
192
|
+
result.push(...codeBlock);
|
|
193
|
+
result.push('```');
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
result.push(...codeBlock);
|
|
197
|
+
}
|
|
198
|
+
codeBlock = [];
|
|
199
|
+
inCode = false;
|
|
200
|
+
}
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
const trimmed = line.trimEnd();
|
|
203
|
+
// Track fenced code blocks — pass them through untouched
|
|
204
|
+
if (trimmed.startsWith('```')) {
|
|
205
|
+
if (inCode)
|
|
206
|
+
flushCode();
|
|
207
|
+
inFence = !inFence;
|
|
208
|
+
result.push(line);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (inFence) {
|
|
212
|
+
result.push(line);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const looksLikeCode = CODE_PATTERNS.test(trimmed) || (inCode && (trimmed === '' || /^\s{2,}\S/.test(trimmed)));
|
|
216
|
+
if (looksLikeCode) {
|
|
217
|
+
inCode = true;
|
|
218
|
+
codeBlock.push(line);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
if (inCode)
|
|
222
|
+
flushCode();
|
|
223
|
+
result.push(line);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (inCode)
|
|
227
|
+
flushCode();
|
|
228
|
+
return result.join('\n');
|
|
229
|
+
}
|
|
230
|
+
/** Module-level artifact store — accumulates across prompts in one shell session. */
|
|
231
|
+
let _sessionArtifacts = [];
|
|
232
|
+
export function getSessionArtifacts() {
|
|
233
|
+
return [..._sessionArtifacts];
|
|
234
|
+
}
|
|
235
|
+
export function clearSessionArtifacts() {
|
|
236
|
+
_sessionArtifacts = [];
|
|
237
|
+
}
|
|
238
|
+
export function createStreamRenderer(sessionId, prompt, steeringBar) {
|
|
239
|
+
let spinner = null;
|
|
240
|
+
let content = '';
|
|
241
|
+
let hasOutput = false;
|
|
242
|
+
let isGroupChat = false; // Set when session_start has multiple agents
|
|
243
|
+
let lastThinkingContent = ''; // Track to dedup with answer event
|
|
244
|
+
let completePrinted = false; // Prevent duplicate timing lines
|
|
245
|
+
const seenToolResults = new Set(); // Dedup tool_result entries
|
|
246
|
+
let tokenStreamed = false; // Track if answer was delivered via token events
|
|
247
|
+
let tokensStarted = false; // Once true, spinner is permanently disabled for this session
|
|
248
|
+
let lastStage = ''; // Current pipeline stage — shown in heartbeat spinner
|
|
249
|
+
// ── Session trace collection ──
|
|
250
|
+
const traceEvents = [];
|
|
251
|
+
const traceToolCalls = [];
|
|
252
|
+
const traceThinking = [];
|
|
253
|
+
const traceErrors = [];
|
|
254
|
+
let traceContextSources = {};
|
|
255
|
+
let traceModel = '';
|
|
256
|
+
let traceAgent = '';
|
|
257
|
+
let traceTurns = 0;
|
|
258
|
+
let lastKnownMaxTurns = '?'; // Populated by turn_progress
|
|
259
|
+
const startedAt = new Date().toISOString();
|
|
260
|
+
const startTime = Date.now();
|
|
261
|
+
function stopSpinner() {
|
|
262
|
+
if (steeringBar?.active) {
|
|
263
|
+
steeringBar.clearStatus();
|
|
264
|
+
}
|
|
265
|
+
if (spinner) {
|
|
266
|
+
spinner.stop();
|
|
267
|
+
spinner = null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Start or update the spinner with new text.
|
|
272
|
+
*
|
|
273
|
+
* When SteeringBar is active, status is shown INSIDE the bar (no ora).
|
|
274
|
+
* This prevents stderr (ora) from fighting stdout (bar) for cursor control.
|
|
275
|
+
*
|
|
276
|
+
* Once token streaming begins, the spinner is permanently disabled.
|
|
277
|
+
*/
|
|
278
|
+
function startSpinner(text) {
|
|
279
|
+
if (tokensStarted)
|
|
280
|
+
return;
|
|
281
|
+
// When SteeringBar is active, show status in the bar — no ora
|
|
282
|
+
if (steeringBar?.active) {
|
|
283
|
+
// Kill any lingering ora instance
|
|
284
|
+
if (spinner) {
|
|
285
|
+
spinner.stop();
|
|
286
|
+
spinner = null;
|
|
287
|
+
}
|
|
288
|
+
steeringBar.setStatus(text.replace(/\x1b\[[0-9;]*m/g, '')); // strip ANSI colors
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (spinner) {
|
|
292
|
+
spinner.text = text;
|
|
293
|
+
if (!spinner.isSpinning)
|
|
294
|
+
spinner.start();
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
spinner = ora({ text, spinner: 'dots', discardStdin: false }).start();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function printIndentedBlock(text, indent = ' ', color = chalk.dim) {
|
|
301
|
+
stopSpinner();
|
|
302
|
+
const lines = String(text)
|
|
303
|
+
.replace(/\r\n/g, '\n')
|
|
304
|
+
.split('\n');
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
console.log(color(`${indent}${line}`));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/** console.log wrapper that auto-clears the spinner before writing.
|
|
310
|
+
* Prevents spinner ANSI escape codes from mixing with event output. */
|
|
311
|
+
function log(...args) {
|
|
312
|
+
stopSpinner();
|
|
313
|
+
console.log(...args);
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
onEvent(event) {
|
|
317
|
+
// ── Collect every event for session trace ──
|
|
318
|
+
traceEvents.push({ ...event, data: { ...event.data, _ts: Date.now() } });
|
|
319
|
+
switch (event.type) {
|
|
320
|
+
case 'session_start':
|
|
321
|
+
if (event.data.group && Array.isArray(event.data.group) && event.data.group.length > 1) {
|
|
322
|
+
isGroupChat = true;
|
|
323
|
+
}
|
|
324
|
+
if (event.data.agent)
|
|
325
|
+
traceAgent = event.data.agent;
|
|
326
|
+
if (event.data.model)
|
|
327
|
+
traceModel = event.data.model;
|
|
328
|
+
startSpinner(chalk.dim(`${event.data.agent || 'thinking'}...`));
|
|
329
|
+
break;
|
|
330
|
+
case 'thinking': {
|
|
331
|
+
const rawThought = event.data.thought || event.data.content || '';
|
|
332
|
+
const turnNum = event.data.turn;
|
|
333
|
+
const phase = event.data.phase || '';
|
|
334
|
+
// Clean up <think> tags
|
|
335
|
+
const clean = rawThought.replace(/<\/?think(?:ing)?>/g, '').trim();
|
|
336
|
+
if (phase === 'streaming' && clean) {
|
|
337
|
+
// ── LIVE chain-of-thought streaming ──
|
|
338
|
+
// Stream reasoning tokens as they arrive, like AitherChat does
|
|
339
|
+
stopSpinner();
|
|
340
|
+
// Use carriage return to overwrite previous streaming line
|
|
341
|
+
const preview = clean.split('\n').filter((l) => l.trim()).slice(-3).join(' ').slice(-200);
|
|
342
|
+
process.stdout.write(`\r${chalk.magenta(' 💭 ')}${chalk.dim(preview)}${' '.repeat(20)}`);
|
|
343
|
+
}
|
|
344
|
+
else if (phase === 'complete' && clean) {
|
|
345
|
+
// ── Full reasoning block complete ──
|
|
346
|
+
// Clear the streaming line, then print the full thought
|
|
347
|
+
process.stdout.write('\r\x1b[2K');
|
|
348
|
+
log(chalk.magenta(' 💭 Reasoning complete:'));
|
|
349
|
+
// Show full reasoning, wrapped nicely
|
|
350
|
+
const lines = clean.split('\n').filter((l) => l.trim());
|
|
351
|
+
for (const line of lines) {
|
|
352
|
+
log(chalk.dim(` ${line}`));
|
|
353
|
+
}
|
|
354
|
+
lastThinkingContent = clean;
|
|
355
|
+
traceThinking.push(clean);
|
|
356
|
+
}
|
|
357
|
+
else if (turnNum != null && clean && clean !== 'thinking...' && clean.length > 5) {
|
|
358
|
+
// ── Agentic turn thought ──
|
|
359
|
+
stopSpinner();
|
|
360
|
+
lastThinkingContent = clean;
|
|
361
|
+
const turnLabel = chalk.cyan(` [Turn ${turnNum}]`);
|
|
362
|
+
// Show full reasoning trace, not just first line
|
|
363
|
+
const lines = clean.split('\n').filter((l) => l.trim());
|
|
364
|
+
for (const line of lines) {
|
|
365
|
+
log(`${turnLabel} ${chalk.dim(line)}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else if (clean.length > 10) {
|
|
369
|
+
// Post-completion thinking block — show full chain of thought
|
|
370
|
+
stopSpinner();
|
|
371
|
+
lastStage = 'reasoning';
|
|
372
|
+
const thinkLines = clean.split('\n').filter((l) => l.trim());
|
|
373
|
+
if (thinkLines.length > 8) {
|
|
374
|
+
log(chalk.magenta(' 💭 Chain of thought:'));
|
|
375
|
+
for (const line of thinkLines) {
|
|
376
|
+
log(chalk.dim(` ${line}`));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
for (const line of thinkLines) {
|
|
381
|
+
log(chalk.dim(` 💭 ${line}`));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
traceThinking.push(clean);
|
|
385
|
+
}
|
|
386
|
+
else if (spinner) {
|
|
387
|
+
spinner.text = chalk.dim(clean || 'thinking...');
|
|
388
|
+
}
|
|
389
|
+
else if (!hasOutput) {
|
|
390
|
+
startSpinner(chalk.dim(clean || 'thinking...'));
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case 'thinking_end': {
|
|
395
|
+
// Clear any streaming line remnant
|
|
396
|
+
process.stdout.write('\r\x1b[2K');
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case 'progress':
|
|
400
|
+
case 'status': {
|
|
401
|
+
const msg = event.data.message || event.data.phase || event.data.status || '';
|
|
402
|
+
const phase = event.data.phase || '';
|
|
403
|
+
// Print ALL meaningful pipeline stages as visible lines
|
|
404
|
+
const _printablePhases = new Set([
|
|
405
|
+
'tool_reg_start', 'tool_reg_done', 'discovery_start',
|
|
406
|
+
'notebook_start', 'orchestrator_start', 'runtime_start',
|
|
407
|
+
'affect_start', 'prefire_start', 'facet_plan_start',
|
|
408
|
+
'plan_start', 'planner_start', 'planner_done',
|
|
409
|
+
'research_start',
|
|
410
|
+
// IntentPlanner sub-phases
|
|
411
|
+
'decompose_start', 'tool_match', 'critique_start',
|
|
412
|
+
// Context assembly sub-phases
|
|
413
|
+
'context_neurons', 'context_memory', 'context_graph',
|
|
414
|
+
'context_affect', 'context_flux',
|
|
415
|
+
// Tool execution phases
|
|
416
|
+
'tool_executing',
|
|
417
|
+
]);
|
|
418
|
+
if (_printablePhases.has(phase) && msg) {
|
|
419
|
+
stopSpinner();
|
|
420
|
+
log(chalk.dim(` ⚙ ${msg}`));
|
|
421
|
+
startSpinner(chalk.dim(msg));
|
|
422
|
+
}
|
|
423
|
+
else if (msg) {
|
|
424
|
+
if (steeringBar?.active) {
|
|
425
|
+
steeringBar.setStatus(msg);
|
|
426
|
+
}
|
|
427
|
+
else if (spinner) {
|
|
428
|
+
spinner.text = chalk.dim(msg);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case 'token': {
|
|
434
|
+
stopSpinner();
|
|
435
|
+
tokensStarted = true; // Permanently disable spinner for this response
|
|
436
|
+
const t = decodeHtmlEntities(event.data.t || '');
|
|
437
|
+
content += t;
|
|
438
|
+
process.stdout.write(t);
|
|
439
|
+
hasOutput = true;
|
|
440
|
+
tokenStreamed = true;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case 'message':
|
|
444
|
+
case 'answer':
|
|
445
|
+
case 'final_answer': {
|
|
446
|
+
stopSpinner();
|
|
447
|
+
const answer = event.data.response || event.data.answer || event.data.content || '';
|
|
448
|
+
if (answer) {
|
|
449
|
+
// Dedup: skip if already printed (AgentRuntime emits final_answer,
|
|
450
|
+
// then the router emits answer with the same content)
|
|
451
|
+
if (content && answer.trim() === content.trim()) {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
if (!hasOutput)
|
|
455
|
+
process.stdout.write('\n');
|
|
456
|
+
// Group-chat: label each agent's response when multiple agents respond
|
|
457
|
+
const answerAgent = event.data.agent || '';
|
|
458
|
+
if (isGroupChat && answerAgent) {
|
|
459
|
+
process.stdout.write(chalk.bold.cyan(`\n [${answerAgent}]\n`));
|
|
460
|
+
}
|
|
461
|
+
content = answer;
|
|
462
|
+
// Write answer text immediately — don't block on heavy markdown
|
|
463
|
+
// rendering. Markdown is deferred to the 'complete' handler for
|
|
464
|
+
// responses that contain code blocks.
|
|
465
|
+
process.stdout.write(answer);
|
|
466
|
+
if (!answer.endsWith('\n'))
|
|
467
|
+
process.stdout.write('\n');
|
|
468
|
+
hasOutput = true;
|
|
469
|
+
}
|
|
470
|
+
// Render interactive/display blocks in terminal
|
|
471
|
+
const blocks = event.data.render_blocks;
|
|
472
|
+
if (blocks && Array.isArray(blocks) && blocks.length > 0) {
|
|
473
|
+
process.stdout.write('\n');
|
|
474
|
+
renderTerminalBlocks(blocks);
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case 'render_blocks':
|
|
479
|
+
case 'await_input': {
|
|
480
|
+
stopSpinner();
|
|
481
|
+
const rblocks = event.data.render_blocks || event.data.blocks || [];
|
|
482
|
+
if (rblocks.length > 0) {
|
|
483
|
+
renderTerminalBlocks(rblocks);
|
|
484
|
+
}
|
|
485
|
+
if (event.data.prompt) {
|
|
486
|
+
log(chalk.yellow(`\n ⏳ ${event.data.prompt}`));
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case 'partial': {
|
|
491
|
+
stopSpinner();
|
|
492
|
+
const partial = event.data.content || event.data.text || '';
|
|
493
|
+
if (partial && !hasOutput) {
|
|
494
|
+
content = partial;
|
|
495
|
+
process.stdout.write(partial);
|
|
496
|
+
hasOutput = true;
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case 'tool_call': {
|
|
501
|
+
stopSpinner();
|
|
502
|
+
const turnLabel2 = event.data.turn != null ? chalk.cyan(`[Turn ${event.data.turn}] `) : '';
|
|
503
|
+
const tools = event.data.tools || event.data.tool_calls || [];
|
|
504
|
+
for (const tool of tools) {
|
|
505
|
+
const name = tool.name || tool.function?.name || 'tool';
|
|
506
|
+
const args = tool.args || tool.arguments;
|
|
507
|
+
traceToolCalls.push({ name, args, timestamp: Date.now() });
|
|
508
|
+
// Show the most useful argument (query, url, task, prompt, etc.)
|
|
509
|
+
let argDisplay = '';
|
|
510
|
+
if (args) {
|
|
511
|
+
const key = args.query || args.url || args.task || args.prompt || args.path || args.code;
|
|
512
|
+
if (key) {
|
|
513
|
+
argDisplay = chalk.dim(` → ${String(key)}`);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
argDisplay = chalk.dim(` ${JSON.stringify(args)}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
log(` ${turnLabel2}` + chalk.yellow(`⚡ ${name}`) + argDisplay);
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case 'tool_result': {
|
|
524
|
+
const results = event.data.results || [];
|
|
525
|
+
let anySuccessfulOutput = false;
|
|
526
|
+
for (const result of results) {
|
|
527
|
+
// Dedup: skip identical tool+output combos (LLM may call the
|
|
528
|
+
// same tool twice across follow-up turns)
|
|
529
|
+
const dedupKey = `${result.tool || 'tool'}::${(result.output || '').slice(0, 200)}`;
|
|
530
|
+
if (seenToolResults.has(dedupKey))
|
|
531
|
+
continue;
|
|
532
|
+
seenToolResults.add(dedupKey);
|
|
533
|
+
const icon = result.success !== false ? chalk.green('\u2713') : chalk.red('\u2717');
|
|
534
|
+
const name = result.tool || 'tool';
|
|
535
|
+
if (result.success !== false && result.output)
|
|
536
|
+
anySuccessfulOutput = true;
|
|
537
|
+
if (result.output && result.success !== false) {
|
|
538
|
+
try {
|
|
539
|
+
const parsed = JSON.parse(result.output);
|
|
540
|
+
// ── Search results: expand each result ──
|
|
541
|
+
if (parsed.results && Array.isArray(parsed.results) && parsed.results.length > 0) {
|
|
542
|
+
log(` ${icon} ${chalk.bold(name)} — ${parsed.results.length} results:`);
|
|
543
|
+
if (parsed.answer && parsed.answer.length > 0) {
|
|
544
|
+
printIndentedBlock(`📝 ${decodeHtmlEntities(parsed.answer)}`);
|
|
545
|
+
}
|
|
546
|
+
for (const [idx, r] of parsed.results.entries()) {
|
|
547
|
+
const title = decodeHtmlEntities(r.title || 'Untitled');
|
|
548
|
+
const snippet = decodeHtmlEntities(r.snippet || r.body || '');
|
|
549
|
+
log(chalk.white(` ${idx + 1}. ${title}`));
|
|
550
|
+
if (snippet) {
|
|
551
|
+
printIndentedBlock(snippet, ' ');
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// ── Webpage fetch: show title + clean content preview ──
|
|
556
|
+
else if (parsed.content) {
|
|
557
|
+
const title = parsed.title ? decodeHtmlEntities(parsed.title) + ' — ' : '';
|
|
558
|
+
const cleanContent = stripWebCruft(decodeHtmlEntities(parsed.content));
|
|
559
|
+
log(` ${icon} ${chalk.bold(name)}`);
|
|
560
|
+
printIndentedBlock(`${title}${cleanContent}`);
|
|
561
|
+
}
|
|
562
|
+
// ── Generic JSON ──
|
|
563
|
+
else {
|
|
564
|
+
log(` ${icon} ${chalk.bold(name)}:`);
|
|
565
|
+
printIndentedBlock(JSON.stringify(parsed, null, 2));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
// Non-JSON output
|
|
570
|
+
const lines = String(result.output).split('\n').filter((l) => l.trim());
|
|
571
|
+
log(` ${icon} ${chalk.bold(name)}:`);
|
|
572
|
+
for (const line of lines) {
|
|
573
|
+
printIndentedBlock(line);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else if (result.success === false) {
|
|
578
|
+
const err = result.output || result.error || 'unknown error';
|
|
579
|
+
log(` ${icon} ${chalk.bold(name)}:`);
|
|
580
|
+
printIndentedBlock(String(err), ' ', chalk.red);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
log(` ${icon} ${name}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Mark that we showed tool output — prevents '(no response)'
|
|
587
|
+
// when the LLM's text answer is empty but tools returned data
|
|
588
|
+
if (anySuccessfulOutput)
|
|
589
|
+
hasOutput = true;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
case 'done':
|
|
593
|
+
case 'complete': {
|
|
594
|
+
stopSpinner();
|
|
595
|
+
// Only render content + timing once (engine + router can both emit 'complete')
|
|
596
|
+
if (completePrinted)
|
|
597
|
+
break;
|
|
598
|
+
completePrinted = true;
|
|
599
|
+
if (!hasOutput && event.data.content) {
|
|
600
|
+
content = event.data.content;
|
|
601
|
+
process.stdout.write(renderMarkdown(wrapBareCode(content)));
|
|
602
|
+
hasOutput = true;
|
|
603
|
+
}
|
|
604
|
+
else if (tokenStreamed) {
|
|
605
|
+
// Content was streamed raw via token events — re-render with
|
|
606
|
+
// markdown so code blocks display properly. wrapBareCode catches
|
|
607
|
+
// bare code even when no fences are present.
|
|
608
|
+
const wrapped = wrapBareCode(content);
|
|
609
|
+
if (wrapped !== content || content.includes('```')) {
|
|
610
|
+
process.stdout.write('\n');
|
|
611
|
+
process.stdout.write(renderMarkdown(wrapped));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (hasOutput)
|
|
615
|
+
process.stdout.write('\n');
|
|
616
|
+
const meta = [];
|
|
617
|
+
if (event.data.model || event.data.model_used)
|
|
618
|
+
meta.push(event.data.model || event.data.model_used);
|
|
619
|
+
if (event.data.duration_ms || event.data.elapsed_ms) {
|
|
620
|
+
meta.push(`${Math.round(event.data.duration_ms || event.data.elapsed_ms)}ms`);
|
|
621
|
+
}
|
|
622
|
+
if (event.data.turns_completed)
|
|
623
|
+
meta.push(`${event.data.turns_completed} turns`);
|
|
624
|
+
if (event.data.agent)
|
|
625
|
+
meta.push(event.data.agent);
|
|
626
|
+
if (meta.length) {
|
|
627
|
+
log(chalk.dim(` [${meta.join(' \u00b7 ')}]`));
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case 'error':
|
|
632
|
+
stopSpinner();
|
|
633
|
+
log(chalk.red(`\n Error: ${event.data.error || 'unknown error'}`));
|
|
634
|
+
traceErrors.push(String(event.data.error || 'unknown error'));
|
|
635
|
+
break;
|
|
636
|
+
case 'stream_timeout':
|
|
637
|
+
// Soft timeout — stream went quiet but agent may still be running.
|
|
638
|
+
// Don't show a hard error; the REPL will check forge status.
|
|
639
|
+
stopSpinner();
|
|
640
|
+
hasOutput = true; // Prevent "(no response)" — we already showed trace output
|
|
641
|
+
break;
|
|
642
|
+
case 'heartbeat':
|
|
643
|
+
case 'keepalive': {
|
|
644
|
+
const elapsedMs = event.data.elapsed_ms || event.data.elapsed;
|
|
645
|
+
if (elapsedMs != null) {
|
|
646
|
+
const secs = Math.round(elapsedMs / 1000);
|
|
647
|
+
const elapsed = secs >= 60 ? `${Math.floor(secs / 60)}m ${secs % 60}s` : `${secs}s`;
|
|
648
|
+
const stageHint = lastStage ? ` · ${lastStage}` : '';
|
|
649
|
+
const statusMsg = `Working... ${elapsed} elapsed${stageHint}`;
|
|
650
|
+
if (steeringBar?.active) {
|
|
651
|
+
steeringBar.setStatus(statusMsg);
|
|
652
|
+
}
|
|
653
|
+
else if (spinner) {
|
|
654
|
+
spinner.text = chalk.dim(statusMsg);
|
|
655
|
+
}
|
|
656
|
+
if (secs > 0 && secs % 30 === 0) {
|
|
657
|
+
stopSpinner();
|
|
658
|
+
log(chalk.dim(` ⏱ Still working... ${elapsed}${stageHint}`));
|
|
659
|
+
startSpinner(chalk.dim(statusMsg));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
const stageHint = lastStage ? ` · ${lastStage}` : '';
|
|
664
|
+
if (steeringBar?.active) {
|
|
665
|
+
steeringBar.setStatus(`Working...${stageHint}`);
|
|
666
|
+
}
|
|
667
|
+
else if (spinner) {
|
|
668
|
+
spinner.text = chalk.dim(`Working...${stageHint}`);
|
|
669
|
+
}
|
|
670
|
+
else if (!hasOutput) {
|
|
671
|
+
startSpinner(chalk.dim('Working...'));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
case 'debug':
|
|
677
|
+
break; // silent
|
|
678
|
+
// ── LLM lifecycle events — show what the model is doing ──
|
|
679
|
+
case 'llm_start': {
|
|
680
|
+
const llmModel = event.data.model || 'auto';
|
|
681
|
+
const llmTurn = event.data.turn;
|
|
682
|
+
const turnHint = llmTurn != null ? ` (turn ${llmTurn})` : '';
|
|
683
|
+
lastStage = `inference: ${llmModel}`;
|
|
684
|
+
startSpinner(chalk.dim(`🧠 LLM inference: ${llmModel}${turnHint}`));
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case 'llm_done': {
|
|
688
|
+
const llmMs = event.data.llm_time_ms || event.data.elapsed_ms || event.data.duration_ms || 0;
|
|
689
|
+
const llmTokens = event.data.tokens_used || event.data.tokens || event.data.completion_tokens || 0;
|
|
690
|
+
const llmMod = event.data.model_used || event.data.model || '';
|
|
691
|
+
const hasTools = event.data.has_tool_calls;
|
|
692
|
+
stopSpinner();
|
|
693
|
+
const parts = [];
|
|
694
|
+
if (llmMs > 0)
|
|
695
|
+
parts.push(`${Math.round(llmMs)}ms`);
|
|
696
|
+
if (llmTokens > 0)
|
|
697
|
+
parts.push(`${llmTokens} tokens`);
|
|
698
|
+
if (llmMod)
|
|
699
|
+
parts.push(llmMod);
|
|
700
|
+
if (hasTools)
|
|
701
|
+
parts.push('+ tool calls');
|
|
702
|
+
if (parts.length > 0) {
|
|
703
|
+
log(chalk.dim(` ⚡ LLM: ${parts.join(' · ')}`));
|
|
704
|
+
}
|
|
705
|
+
lastStage = '';
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case 'llm_error': {
|
|
709
|
+
stopSpinner();
|
|
710
|
+
const llmErr = event.data.error || 'inference failed';
|
|
711
|
+
log(chalk.red(` ⚡ LLM error: ${llmErr}`));
|
|
712
|
+
traceErrors.push(`LLM: ${llmErr}`);
|
|
713
|
+
lastStage = '';
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
// ── Pipeline trace events — show in spinner ──
|
|
717
|
+
case 'classify': {
|
|
718
|
+
const intent = event.data.intent?.type || '?';
|
|
719
|
+
const effort = event.data.effort?.level || '?';
|
|
720
|
+
const tier = event.data.effort?.tier || '';
|
|
721
|
+
const followupBoost = event.data.effort?.followup_boost || 0;
|
|
722
|
+
const oneshot = event.data.effort?.oneshot || false;
|
|
723
|
+
const depth = event.data.depth_label || '';
|
|
724
|
+
let effortLine = ` 📋 Intent: ${intent} | Effort: ${effort}`;
|
|
725
|
+
if (tier)
|
|
726
|
+
effortLine += ` | Tier: ${tier}`;
|
|
727
|
+
if (depth)
|
|
728
|
+
effortLine += ` | Depth: ${depth}`;
|
|
729
|
+
if (followupBoost > 0)
|
|
730
|
+
effortLine += ` (+${followupBoost} follow-up)`;
|
|
731
|
+
if (oneshot)
|
|
732
|
+
effortLine += ` ⚡ one-shot`;
|
|
733
|
+
log(chalk.dim(effortLine));
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
// promotion_suggested handled below (near line 1050+)
|
|
737
|
+
case 'classify_update': {
|
|
738
|
+
const updatedIntent = event.data.intent?.type || '?';
|
|
739
|
+
const updatedEffort = event.data.effort?.level || '?';
|
|
740
|
+
const reason = event.data.reason || 'context';
|
|
741
|
+
log(chalk.dim(` 📋 Intent: ${updatedIntent} | Effort: ${updatedEffort} (${reason})`));
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
case 'model_select': {
|
|
745
|
+
const model = event.data.model_selection?.recommended_model || 'auto';
|
|
746
|
+
const tier = event.data.model_selection?.tier || 'auto';
|
|
747
|
+
log(chalk.dim(` 🎯 Model: ${model} (${tier})`));
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
case 'context_assembly': {
|
|
751
|
+
const tokens = event.data.total_tokens || 0;
|
|
752
|
+
const gatherMs = event.data.gather_time_ms || 0;
|
|
753
|
+
const sources = event.data.sources || {};
|
|
754
|
+
traceContextSources = sources;
|
|
755
|
+
const sourceEntries = Object.entries(sources);
|
|
756
|
+
if (sourceEntries.length > 0) {
|
|
757
|
+
stopSpinner();
|
|
758
|
+
log(chalk.dim(` 📦 Context assembled: ${tokens} tokens (${gatherMs}ms)`));
|
|
759
|
+
for (const [name, info] of sourceEntries) {
|
|
760
|
+
const si = info;
|
|
761
|
+
const toks = si.tokens || 0;
|
|
762
|
+
const extra = si.fired ? ` (${si.fired} fired)` : '';
|
|
763
|
+
const bar = toks > 0 ? chalk.green('█'.repeat(Math.min(Math.ceil(toks / 50), 20))) : chalk.red('—');
|
|
764
|
+
log(chalk.dim(` ${bar} ${name}: ${toks} tokens${extra}`));
|
|
765
|
+
}
|
|
766
|
+
startSpinner(chalk.dim('Building prompt...'));
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
log(chalk.dim(` 📦 Context: ${tokens} tokens`));
|
|
770
|
+
}
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
case 'context_xray': {
|
|
774
|
+
// Rich context introspection — show what made it into the prompt
|
|
775
|
+
const snapshot = event.data.snapshot_id || '';
|
|
776
|
+
const totalCtx = event.data.total_context_tokens || 0;
|
|
777
|
+
const breakdown = event.data.breakdown || {};
|
|
778
|
+
if (Object.keys(breakdown).length > 0) {
|
|
779
|
+
log(chalk.dim(` 🔬 Context X-Ray: ${totalCtx} tokens ${snapshot ? `(${snapshot})` : ''}`));
|
|
780
|
+
for (const [k, v] of Object.entries(breakdown)) {
|
|
781
|
+
log(chalk.dim(` ${k}: ${v}`));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
case 'context_summary': {
|
|
787
|
+
const origTokens = event.data.original_tokens || 0;
|
|
788
|
+
const summTokens = event.data.summary_tokens || 0;
|
|
789
|
+
if (origTokens > 0) {
|
|
790
|
+
const pct = summTokens > 0 ? Math.round((1 - summTokens / origTokens) * 100) : 0;
|
|
791
|
+
log(chalk.dim(` 📝 Context compressed: ${origTokens} → ${summTokens} tokens (${pct}% reduction)`));
|
|
792
|
+
}
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
case 'clarification_needed': {
|
|
796
|
+
stopSpinner();
|
|
797
|
+
const questions = event.data.questions || [];
|
|
798
|
+
const planSummary = event.data.plan_summary || '';
|
|
799
|
+
if (planSummary) {
|
|
800
|
+
log(chalk.cyan(`\n 📋 Plan: `) + chalk.dim(planSummary));
|
|
801
|
+
}
|
|
802
|
+
if (questions.length > 0) {
|
|
803
|
+
log(chalk.yellow(`\n ⏸ Clarification needed:`));
|
|
804
|
+
for (const [i, q] of questions.entries()) {
|
|
805
|
+
const qText = typeof q === 'string' ? q : (q.question || q.text || '?');
|
|
806
|
+
const opts = (typeof q === 'object' && q.suggested_options) ? chalk.dim(` Options: ${q.suggested_options.join(', ')}`) : '';
|
|
807
|
+
log(chalk.white(` ${i + 1}. ${qText}`) + opts);
|
|
808
|
+
}
|
|
809
|
+
log(chalk.dim('\n 💡 Just type your answer — it will continue the plan automatically.'));
|
|
810
|
+
}
|
|
811
|
+
hasOutput = true;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case 'plan_refined': {
|
|
815
|
+
stopSpinner();
|
|
816
|
+
const refined = event.data.plan_summary || '';
|
|
817
|
+
const executable = event.data.is_executable ? chalk.green(' [ready]') : '';
|
|
818
|
+
if (refined) {
|
|
819
|
+
log(chalk.cyan(`\n 📋 Plan refined: `) + chalk.dim(refined) + executable);
|
|
820
|
+
}
|
|
821
|
+
// Restart spinner so heartbeats keep showing liveness
|
|
822
|
+
startSpinner(chalk.dim('Executing plan...'));
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
case 'plan_ready': {
|
|
826
|
+
stopSpinner();
|
|
827
|
+
// Backend sends summary string, not steps array
|
|
828
|
+
const summary = event.data.summary || '';
|
|
829
|
+
const steps = event.data.steps || [];
|
|
830
|
+
if (summary) {
|
|
831
|
+
const agentic = event.data.agentic ? chalk.cyan(' [agentic]') : '';
|
|
832
|
+
log(chalk.cyan(`\n 📋 Plan: `) + chalk.dim(summary) + agentic);
|
|
833
|
+
}
|
|
834
|
+
else if (steps.length > 0) {
|
|
835
|
+
log(chalk.cyan(`\n 📋 Plan: ${steps.length} steps`));
|
|
836
|
+
for (const [i, step] of steps.entries()) {
|
|
837
|
+
const task = typeof step === 'string' ? step : (step.task || step.description || '?');
|
|
838
|
+
log(chalk.dim(` ${i + 1}. ${task}`));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// Restart spinner so heartbeats keep showing liveness
|
|
842
|
+
startSpinner(chalk.dim('Executing plan...'));
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
case 'plan_step': {
|
|
846
|
+
const icon = event.data.status === 'complete' ? chalk.green('✓') : chalk.yellow('→');
|
|
847
|
+
log(chalk.dim(` ${icon} ${event.data.step || '?'}`));
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
case 'approval_required': {
|
|
851
|
+
stopSpinner();
|
|
852
|
+
log(chalk.yellow(`\n ⏸ Approval required: ${event.data.action || event.data.reason || 'plan review'}`));
|
|
853
|
+
log(chalk.dim(' (use web UI to respond, or /approve to continue)'));
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
case 'tool_selection': {
|
|
857
|
+
const tools = (event.data.tool_names || event.data.detected_categories || []).join(', ') || 'none';
|
|
858
|
+
const strategy = event.data.strategy || '';
|
|
859
|
+
log(chalk.dim(` 🔧 Tools: [${tools}]${strategy ? ` strategy=${strategy}` : ''}`));
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
case 'tool_loop_step': {
|
|
863
|
+
// Individual tool completion within the multi-turn tool loop
|
|
864
|
+
const stepName = event.data.tool_name || 'tool';
|
|
865
|
+
const stepOk = event.data.success !== false;
|
|
866
|
+
const stepMs = event.data.elapsed_ms || 0;
|
|
867
|
+
const stepIcon = stepOk ? chalk.green('\u2713') : chalk.red('\u2717');
|
|
868
|
+
const msHint = stepMs > 0 ? chalk.dim(` (${stepMs}ms)`) : '';
|
|
869
|
+
log(` ${stepIcon} ${chalk.bold(stepName)}${msHint}`);
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
case 'checkpoint': {
|
|
873
|
+
const turn = event.data.turn || '?';
|
|
874
|
+
const maxTurns = event.data.max_turns || lastKnownMaxTurns || '?';
|
|
875
|
+
if (event.data.max_turns)
|
|
876
|
+
lastKnownMaxTurns = event.data.max_turns;
|
|
877
|
+
const ok = event.data.ok || 0;
|
|
878
|
+
const errors = event.data.errors || 0;
|
|
879
|
+
const totalCalls = event.data.total_tool_calls || 0;
|
|
880
|
+
const budgetPct = event.data.budget_pct || 0;
|
|
881
|
+
const elapsed = event.data.elapsed_ms ? `${event.data.elapsed_ms}ms` : '';
|
|
882
|
+
// Show progress bar
|
|
883
|
+
const filled = Math.round(budgetPct / 10);
|
|
884
|
+
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
885
|
+
const errStr = errors > 0 ? chalk.red(` ${errors} err`) : '';
|
|
886
|
+
log(chalk.dim(` ── Turn ${turn}/${maxTurns} [${bar}] ${ok} ok${errStr} · ${totalCalls} total calls · ${elapsed}`));
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
case 'ooda_observe': {
|
|
890
|
+
stopSpinner();
|
|
891
|
+
const calls = event.data.tool_calls || 0;
|
|
892
|
+
const ok = event.data.successes || 0;
|
|
893
|
+
const turn3 = event.data.turn || '?';
|
|
894
|
+
const maxT = event.data.max_turns || lastKnownMaxTurns || '?';
|
|
895
|
+
log(chalk.dim(` 🔍 observe: Turn ${turn3}/${maxT} — ${ok}/${calls} tools ok`));
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
case 'ooda_decide': {
|
|
899
|
+
const phase = event.data.phase || 'decide';
|
|
900
|
+
const reason = event.data.reason || '';
|
|
901
|
+
// Always show OODA decisions — they explain what the agent is doing
|
|
902
|
+
if (reason) {
|
|
903
|
+
stopSpinner();
|
|
904
|
+
log(chalk.dim(` 🔄 ${phase}: ${reason}`));
|
|
905
|
+
}
|
|
906
|
+
else if (spinner) {
|
|
907
|
+
spinner.text = chalk.dim(`OODA ${phase}`);
|
|
908
|
+
}
|
|
909
|
+
if (event.data.strip_tools) {
|
|
910
|
+
log(chalk.dim(' 🔄 Synthesizing from gathered data...'));
|
|
911
|
+
}
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
case 'ooda_delegate': {
|
|
915
|
+
stopSpinner();
|
|
916
|
+
const target = event.data.delegate_to || '?';
|
|
917
|
+
const reason = event.data.reason || '';
|
|
918
|
+
log(chalk.yellow(` 🤝 Delegation → ${target}: ${reason}`));
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
case 'loop_guard': {
|
|
922
|
+
log(chalk.red(` ⛔ Loop guard: ${event.data.tool} — ${event.data.reason || 'blocked'}`));
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
case 'speculative_fire': {
|
|
926
|
+
stopSpinner();
|
|
927
|
+
const specTools = (event.data.tools || []).join(', ');
|
|
928
|
+
log(chalk.cyan(` 🚀 Pre-firing: [${specTools}] (parallel speculative execution)`));
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
case 'speculative_result': {
|
|
932
|
+
stopSpinner();
|
|
933
|
+
const fired = event.data.tools_fired?.length || 0;
|
|
934
|
+
const succeeded = event.data.tools_succeeded?.length || 0;
|
|
935
|
+
const chars = event.data.total_chars || 0;
|
|
936
|
+
log(chalk.green(` ✓ Pre-fetched: ${succeeded}/${fired} tools returned data (${chars} chars)`));
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
case 'turn_start': {
|
|
940
|
+
// Start spinner for new agent turn
|
|
941
|
+
startSpinner(chalk.dim(`Turn ${event.data.turn || '?'}...`));
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
case 'escalation': {
|
|
945
|
+
stopSpinner();
|
|
946
|
+
log(chalk.yellow(` ⬆ Escalation #${event.data.attempt || 1}: ${event.data.reason || 'quality'}`));
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
case 'reasoning_start':
|
|
950
|
+
case 'reasoning_engage': {
|
|
951
|
+
const rawDepth = event.data.depth || 'deep';
|
|
952
|
+
// Map internal labels to user-friendly display
|
|
953
|
+
const depth = rawDepth === 'skip' ? 'light' : rawDepth === 'none' ? 'light' : rawDepth;
|
|
954
|
+
const reason = event.data.reason || event.data.content || '';
|
|
955
|
+
if (spinner) {
|
|
956
|
+
spinner.text = chalk.dim(`Reasoning (${depth}): ${reason || '...'}`);
|
|
957
|
+
}
|
|
958
|
+
// Show reasoning trace content when available
|
|
959
|
+
if (reason.length > 30) {
|
|
960
|
+
log(chalk.dim(` 🧠 Reasoning (${depth}): ${reason}`));
|
|
961
|
+
}
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
case 'reasoning_strategy': {
|
|
965
|
+
stopSpinner();
|
|
966
|
+
const index = event.data.strategy != null ? `#${event.data.strategy}` : '';
|
|
967
|
+
const name = event.data.name || event.data.strategy_name || 'strategy';
|
|
968
|
+
const detail = event.data.detail || event.data.content || '';
|
|
969
|
+
log(chalk.dim(` 🧠 Strategy ${index} ${name}`.trimEnd()));
|
|
970
|
+
if (detail) {
|
|
971
|
+
printIndentedBlock(detail);
|
|
972
|
+
}
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
case 'reasoning_trace':
|
|
976
|
+
case 'reasoning_step': {
|
|
977
|
+
const step = event.data.content || event.data.step || event.data.trace || '';
|
|
978
|
+
if (step) {
|
|
979
|
+
stopSpinner();
|
|
980
|
+
log(chalk.dim(' 🧠 Reasoning step:'));
|
|
981
|
+
printIndentedBlock(step);
|
|
982
|
+
}
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
case 'agentic_upgrade': {
|
|
986
|
+
stopSpinner();
|
|
987
|
+
const reason = event.data.reason || 'auto';
|
|
988
|
+
log(chalk.cyan(` 🔀 Agentic mode: ${reason}`));
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
case 'promotion_suggested': {
|
|
992
|
+
stopSpinner();
|
|
993
|
+
const sugEffort = event.data.suggested_effort || '?';
|
|
994
|
+
const sugReason = event.data.reason || '';
|
|
995
|
+
const sugMsg = event.data.message || '';
|
|
996
|
+
log(chalk.yellow(`\n ⬆ ${sugMsg || `Suggest escalation to effort ${sugEffort}: ${sugReason}`}`));
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
case 'council':
|
|
1000
|
+
case 'council_perspective': {
|
|
1001
|
+
const agent = event.data.agent || 'council';
|
|
1002
|
+
const content = event.data.perspective || event.data.consensus || '';
|
|
1003
|
+
if (content) {
|
|
1004
|
+
log(chalk.magenta(` 👥 ${agent}: `) + chalk.dim(content));
|
|
1005
|
+
}
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
case 'agent_message': {
|
|
1009
|
+
const agentName = event.data.agent || 'agent';
|
|
1010
|
+
log(chalk.blue(` 🤖 ${agentName}: `) + chalk.dim(event.data.content || ''));
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
case 'steering_guide': {
|
|
1014
|
+
log(chalk.yellow(` 🔄 Steering: ${event.data.hook || '?'} → ${event.data.reason || 'guided'}`));
|
|
1015
|
+
if (event.data.suggestion) {
|
|
1016
|
+
log(chalk.dim(` 💡 ${event.data.suggestion}`));
|
|
1017
|
+
}
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
case 'think_start': {
|
|
1021
|
+
// Show think configuration as a visible pipeline trace
|
|
1022
|
+
const model = event.data.model || 'auto';
|
|
1023
|
+
const effort = event.data.effort_level || '?';
|
|
1024
|
+
const reasoning = event.data.use_reasoning ? 'reasoning' : 'standard';
|
|
1025
|
+
const elapsed = event.data.elapsed_ms ? `${event.data.elapsed_ms}ms` : '';
|
|
1026
|
+
log(chalk.dim(` ⚙ Think: E${effort} ${reasoning} model=${model} ${elapsed}`));
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
// ── UCB pipeline stages — context gathering + LLM call ──
|
|
1030
|
+
case 'context_start': {
|
|
1031
|
+
const sources = (event.data.sources || []).join(', ') || 'none';
|
|
1032
|
+
startSpinner(chalk.dim(`Gathering context: ${sources}`));
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
case 'context_done': {
|
|
1036
|
+
const gatherMs2 = event.data.gather_time_ms || 0;
|
|
1037
|
+
const sourceResults = event.data.sources || {};
|
|
1038
|
+
const sourceNames = Object.keys(sourceResults);
|
|
1039
|
+
const neuronsFired = event.data.neurons_fired || 0;
|
|
1040
|
+
const cacheHits = event.data.cache_hits || 0;
|
|
1041
|
+
const ctxTokens = event.data.context_tokens || event.data.system_prompt_tokens || 0;
|
|
1042
|
+
if (sourceNames.length > 0) {
|
|
1043
|
+
stopSpinner();
|
|
1044
|
+
const extras = [];
|
|
1045
|
+
if (neuronsFired > 0)
|
|
1046
|
+
extras.push(`${neuronsFired} neurons`);
|
|
1047
|
+
if (cacheHits > 0)
|
|
1048
|
+
extras.push(`${cacheHits} cache hits`);
|
|
1049
|
+
if (ctxTokens > 0)
|
|
1050
|
+
extras.push(`${ctxTokens} tokens`);
|
|
1051
|
+
const extStr = extras.length > 0 ? ` · ${extras.join(', ')}` : '';
|
|
1052
|
+
log(chalk.dim(` 📦 Context gathered: ${sourceNames.join(', ')} (${gatherMs2}ms${extStr})`));
|
|
1053
|
+
// Show token breakdown per source
|
|
1054
|
+
for (const [name, info] of Object.entries(sourceResults)) {
|
|
1055
|
+
const si = info;
|
|
1056
|
+
const toks = si.tokens || 0;
|
|
1057
|
+
if (toks > 0) {
|
|
1058
|
+
const bar = chalk.green('█'.repeat(Math.min(Math.ceil(toks / 100), 20)));
|
|
1059
|
+
const extra = si.fired ? ` (${si.fired} fired)` : si.count ? ` (${si.count} entries)` : '';
|
|
1060
|
+
log(chalk.dim(` ${bar} ${name}: ${toks} tokens${extra}`));
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
case 'llm_start': {
|
|
1067
|
+
// From UCB: has model, prompt_tokens_est, has_tools
|
|
1068
|
+
// From AgentRuntime: has turn, timestamp (minimal)
|
|
1069
|
+
const llmModel = event.data.model || 'auto';
|
|
1070
|
+
traceModel = llmModel;
|
|
1071
|
+
const promptTokens = event.data.prompt_tokens_est || 0;
|
|
1072
|
+
const hasTools2 = event.data.has_tools ? ' + tools' : '';
|
|
1073
|
+
const turnInfo = event.data.turn ? `Turn ${event.data.turn} · ` : '';
|
|
1074
|
+
const tokenInfo = promptTokens > 0 ? ` (${promptTokens} tokens${hasTools2})` : '';
|
|
1075
|
+
const spinText = `${turnInfo}LLM inference: ${llmModel}${tokenInfo}`;
|
|
1076
|
+
lastStage = `LLM: ${llmModel}`;
|
|
1077
|
+
// Always print a visible line so the user knows what's happening
|
|
1078
|
+
stopSpinner();
|
|
1079
|
+
log(chalk.dim(` 🧠 ${spinText}`));
|
|
1080
|
+
startSpinner(chalk.dim(spinText));
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
case 'llm_done':
|
|
1084
|
+
case 'llm_end': {
|
|
1085
|
+
stopSpinner();
|
|
1086
|
+
const llmMs = event.data.llm_time_ms || event.data.duration_ms || 0;
|
|
1087
|
+
const tokensUsed = event.data.tokens_used || event.data.tokens || 0;
|
|
1088
|
+
const llmModelUsed = event.data.model_used || event.data.model || 'default';
|
|
1089
|
+
const hasThinking = event.data.has_thinking ? ' + reasoning' : '';
|
|
1090
|
+
const hasToolCalls = event.data.has_tool_calls ? ' + tool_calls' : '';
|
|
1091
|
+
log(chalk.dim(` ⚡ LLM: ${llmModelUsed} → ${tokensUsed} tokens (${Math.round(llmMs)}ms)${hasThinking}${hasToolCalls}`));
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
case 'middleware_progress': {
|
|
1095
|
+
const mwName = event.data.middleware || event.data.stage || 'middleware';
|
|
1096
|
+
const mwStatus = event.data.status || 'running';
|
|
1097
|
+
const mwDetail = event.data.detail || '';
|
|
1098
|
+
lastStage = mwName;
|
|
1099
|
+
const mwIcon = mwStatus === 'complete' ? chalk.green('✓') : chalk.yellow('→');
|
|
1100
|
+
const detailStr = mwDetail ? chalk.dim(` (${mwDetail})`) : '';
|
|
1101
|
+
if (steeringBar?.active) {
|
|
1102
|
+
// Log line goes through interceptor (one bar redraw).
|
|
1103
|
+
// Status updates without extra redraws.
|
|
1104
|
+
console.log(chalk.dim(` ${mwIcon} ${mwName}: ${mwStatus}${detailStr}`));
|
|
1105
|
+
steeringBar.setStatus(`${mwName}...`);
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
stopSpinner();
|
|
1109
|
+
log(chalk.dim(` ${mwIcon} ${mwName}: ${mwStatus}${detailStr}`));
|
|
1110
|
+
startSpinner(chalk.dim(`${mwName}...`));
|
|
1111
|
+
}
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
case 'mcts_plan': {
|
|
1115
|
+
stopSpinner();
|
|
1116
|
+
const mSteps = event.data.steps || [];
|
|
1117
|
+
const mMethodRaw = event.data.method || 'MCTS';
|
|
1118
|
+
const mMethodLabels = {
|
|
1119
|
+
'direct': 'direct',
|
|
1120
|
+
'mcts_heuristic': 'mcts_heuristic',
|
|
1121
|
+
'mcts': 'mcts',
|
|
1122
|
+
};
|
|
1123
|
+
const mMethod = mMethodLabels[mMethodRaw] || mMethodRaw;
|
|
1124
|
+
log(chalk.cyan(` 🌲 ${mMethod} plan: ${mSteps.length} steps`));
|
|
1125
|
+
for (const [i, step] of mSteps.entries()) {
|
|
1126
|
+
const desc = typeof step === 'string' ? step : (step.task || step.description || step.type || '?');
|
|
1127
|
+
const tool = (typeof step === 'object' && step.tool) ? chalk.yellow(` [${step.tool}]`) : '';
|
|
1128
|
+
log(chalk.dim(` ${i + 1}. ${desc}`) + tool);
|
|
1129
|
+
}
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
// ── Context Facets — multi-phase agentic execution ──
|
|
1133
|
+
case 'facet_start': {
|
|
1134
|
+
stopSpinner();
|
|
1135
|
+
const chType = (event.data.facet_type || 'unknown').toUpperCase();
|
|
1136
|
+
const chIdx = event.data.facet_index ?? '?';
|
|
1137
|
+
const chTurns = event.data.max_turns || '?';
|
|
1138
|
+
const chTools = Array.isArray(event.data.tools) ? event.data.tools.length + ' tools' : 'all tools';
|
|
1139
|
+
log(chalk.cyan(`\n ── Facet ${chIdx}: ${chType} (${chTurns} turns, ${chTools}) ──`));
|
|
1140
|
+
startSpinner(chalk.dim(`${chType} phase...`));
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
case 'facet_end': {
|
|
1144
|
+
stopSpinner();
|
|
1145
|
+
const endType = (event.data.facet_type || '').toUpperCase();
|
|
1146
|
+
const endTurns = event.data.turns_completed || 0;
|
|
1147
|
+
const endTokens = event.data.tokens_used || 0;
|
|
1148
|
+
const endRemaining = event.data.tokens_remaining || 0;
|
|
1149
|
+
const crystalPreview = event.data.crystal_summary || '';
|
|
1150
|
+
log(chalk.green(` ✓ ${endType} complete: ${endTurns} turns, ${endTokens} tokens${endRemaining > 0 ? ` (+${endRemaining} unspent)` : ''}`));
|
|
1151
|
+
if (crystalPreview) {
|
|
1152
|
+
log(chalk.dim(' Crystal:'));
|
|
1153
|
+
printIndentedBlock(crystalPreview, ' ');
|
|
1154
|
+
}
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
case 'facet_crystallize': {
|
|
1158
|
+
const crIdx = event.data.facet_index ?? '?';
|
|
1159
|
+
const findings = event.data.key_findings || [];
|
|
1160
|
+
if (findings.length > 0) {
|
|
1161
|
+
log(chalk.yellow(` 💎 Crystallized ${findings.length} findings from facet ${crIdx}:`));
|
|
1162
|
+
for (const f of findings) {
|
|
1163
|
+
printIndentedBlock(`• ${f}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
case 'budget_cascade': {
|
|
1169
|
+
const fromCh = event.data.from_facet || event.data.from_chapter || '?';
|
|
1170
|
+
const toCh = event.data.to_facet || event.data.to_chapter || 'pool (reusable for later facets)';
|
|
1171
|
+
const transferred = event.data.tokens_transferred || 0;
|
|
1172
|
+
if (transferred > 0) {
|
|
1173
|
+
log(chalk.dim(` ↪ Budget cascade: ${transferred} turns from ${fromCh} → ${toCh}`));
|
|
1174
|
+
}
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
case 'artifact_delivered': {
|
|
1178
|
+
stopSpinner();
|
|
1179
|
+
const artFile = event.data.filename || event.data.path || 'file';
|
|
1180
|
+
const artSize = event.data.size || 0;
|
|
1181
|
+
const artCmd = event.data.retrieve_cmd || '';
|
|
1182
|
+
const artLang = event.data.language || '';
|
|
1183
|
+
const artPath = event.data.path || '';
|
|
1184
|
+
// Track artifact in session store
|
|
1185
|
+
_sessionArtifacts.push({
|
|
1186
|
+
filename: artFile,
|
|
1187
|
+
path: artPath,
|
|
1188
|
+
size: artSize,
|
|
1189
|
+
language: artLang,
|
|
1190
|
+
retrieve_cmd: artCmd,
|
|
1191
|
+
timestamp: new Date().toISOString(),
|
|
1192
|
+
});
|
|
1193
|
+
const artIdx = _sessionArtifacts.length;
|
|
1194
|
+
log(chalk.green.bold(`\n 📦 Artifact #${artIdx}: ${artFile} (${artSize} bytes)`));
|
|
1195
|
+
if (artCmd) {
|
|
1196
|
+
log(chalk.dim(` Retrieve: ${artCmd}`));
|
|
1197
|
+
log(chalk.cyan(` Run /artifacts to list all, /get ${artIdx} to download`));
|
|
1198
|
+
}
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
// Pipeline stages that update spinner
|
|
1202
|
+
case 'pipeline':
|
|
1203
|
+
case 'session_learn':
|
|
1204
|
+
case 'search_fire':
|
|
1205
|
+
case 'judge':
|
|
1206
|
+
case 'delegation_decide':
|
|
1207
|
+
case 'fast_path':
|
|
1208
|
+
case 'security_gate':
|
|
1209
|
+
case 'memory_recall':
|
|
1210
|
+
case 'guardrail_check': {
|
|
1211
|
+
if (spinner) {
|
|
1212
|
+
const msg = event.data.message || event.data.stage || event.data.phase
|
|
1213
|
+
|| event.data.status || event.type;
|
|
1214
|
+
spinner.text = chalk.dim(msg);
|
|
1215
|
+
}
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
// ── Missing event handlers — full observability ──
|
|
1219
|
+
case 'turn_end': {
|
|
1220
|
+
const teTurn = event.data.turn || '?';
|
|
1221
|
+
const teDuration = event.data.duration_ms ? `${Math.round(event.data.duration_ms)}ms` : '';
|
|
1222
|
+
const teOutcome = event.data.outcome || '';
|
|
1223
|
+
log(chalk.dim(` ── Turn ${teTurn} end${teOutcome ? `: ${teOutcome}` : ''}${teDuration ? ` (${teDuration})` : ''}`));
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
case 'tool_denied': {
|
|
1227
|
+
const denied = event.data.denied || [];
|
|
1228
|
+
if (denied.length > 0) {
|
|
1229
|
+
log(chalk.red(` ⛔ Tools denied: ${denied.join(', ')}`));
|
|
1230
|
+
}
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
case 'tool_selected': {
|
|
1234
|
+
const selToolsArr = event.data.tools || [];
|
|
1235
|
+
const selTool = selToolsArr.length > 0
|
|
1236
|
+
? `[${selToolsArr.slice(0, 4).join(', ')}${selToolsArr.length > 4 ? `, +${selToolsArr.length - 4}` : ''}]`
|
|
1237
|
+
: (event.data.tool || event.data.name || '?');
|
|
1238
|
+
const selTotal = event.data.total_available || 0;
|
|
1239
|
+
const selReason = selTotal > 0 ? `${selToolsArr.length}/${selTotal} tools` : (event.data.reason || '');
|
|
1240
|
+
log(chalk.dim(` 🔧 Tool selected: ${selTool}${selReason ? ` — ${selReason}` : ''}`));
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
case 'llm_error': {
|
|
1244
|
+
stopSpinner();
|
|
1245
|
+
const llmErr = event.data.error || event.data.message || 'unknown';
|
|
1246
|
+
const llmErrModel = event.data.model || '?';
|
|
1247
|
+
log(chalk.red(` ✗ LLM error (${llmErrModel}): ${String(llmErr)}`));
|
|
1248
|
+
traceErrors.push(`LLM error (${llmErrModel}): ${String(llmErr)}`);
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
case 'reasoning_depth': {
|
|
1252
|
+
const rdDepth = event.data.depth || event.data.level || '?';
|
|
1253
|
+
const rdReason = event.data.reason || '';
|
|
1254
|
+
log(chalk.dim(` 🧠 Reasoning depth: ${rdDepth}${rdReason ? ` — ${rdReason}` : ''}`));
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
case 'agentic_promotion': {
|
|
1258
|
+
stopSpinner();
|
|
1259
|
+
const apReason = event.data.reason || 'auto';
|
|
1260
|
+
const apEffort = event.data.effort || '?';
|
|
1261
|
+
log(chalk.cyan(` 🔀 Agentic promotion: effort=${apEffort} — ${apReason}`));
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
case 'steering': {
|
|
1265
|
+
const stHook = event.data.hook || event.data.type || '?';
|
|
1266
|
+
const stReason = event.data.reason || '';
|
|
1267
|
+
log(chalk.yellow(` 🔄 Steering: ${stHook}${stReason ? ` — ${stReason}` : ''}`));
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
case 'strategy_subsystem': {
|
|
1271
|
+
const ssName = event.data.subsystem || event.data.name || '?';
|
|
1272
|
+
const ssStatus = event.data.status || event.data.decision || '';
|
|
1273
|
+
log(chalk.dim(` ⚙ Strategy: ${ssName} → ${ssStatus}`));
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
case 'plan_approved': {
|
|
1277
|
+
log(chalk.green(` ✓ Plan approved${event.data.plan_id ? ` (${event.data.plan_id})` : ''}`));
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
case 'verification': {
|
|
1281
|
+
const vResults = event.data.results || [];
|
|
1282
|
+
if (Array.isArray(vResults) && vResults.length > 0) {
|
|
1283
|
+
for (const v of vResults) {
|
|
1284
|
+
const icon = v.success ? chalk.green('✓') : chalk.red('✗');
|
|
1285
|
+
const path = v.path || '';
|
|
1286
|
+
const kind = v.kind || '';
|
|
1287
|
+
const reason = v.reason || '';
|
|
1288
|
+
const kindLabel = kind ? ` (${kind.replace(/_/g, ' ')})` : '';
|
|
1289
|
+
log(chalk.dim(` 🔍 ${icon} ${path}${kindLabel}${reason ? ` — ${reason}` : ''}`));
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
else {
|
|
1293
|
+
const vResult = event.data.passed ? chalk.green('✓ passed') : chalk.red('✗ failed');
|
|
1294
|
+
const vDetail = event.data.detail || event.data.reason || '';
|
|
1295
|
+
log(chalk.dim(` 🔍 Verification: ${vResult}${vDetail ? ` — ${vDetail}` : ''}`));
|
|
1296
|
+
}
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
case 'image_gen_start': {
|
|
1300
|
+
stopSpinner();
|
|
1301
|
+
const igPrompt = event.data.prompt || event.data.description || '';
|
|
1302
|
+
log(chalk.cyan(` 🎨 Generating image: ${igPrompt}`));
|
|
1303
|
+
startSpinner(chalk.dim('Generating image...'));
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
case 'image_gen_complete': {
|
|
1307
|
+
stopSpinner();
|
|
1308
|
+
const igUrl = event.data.url || '';
|
|
1309
|
+
log(chalk.green(` ✓ Image generated${igUrl ? `: ${igUrl}` : ''}`));
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
case 'image_gen_failed': {
|
|
1313
|
+
stopSpinner();
|
|
1314
|
+
const igErr = event.data.error || 'unknown';
|
|
1315
|
+
log(chalk.red(` ✗ Image generation failed: ${igErr}`));
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
case 'session_context': {
|
|
1319
|
+
const scTopic = event.data.topic || event.data.summary || '';
|
|
1320
|
+
if (scTopic) {
|
|
1321
|
+
log(chalk.dim(` 📝 Session context: ${scTopic}`));
|
|
1322
|
+
}
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
case 'reasoning_backend_health': {
|
|
1326
|
+
const backends = event.data.backends || {};
|
|
1327
|
+
const entries = Object.entries(backends);
|
|
1328
|
+
// Friendly display names for backend IDs
|
|
1329
|
+
const friendlyNames = {
|
|
1330
|
+
'vllm': 'orchestrator vLLM',
|
|
1331
|
+
'vllm_swap': 'local swap (on-demand)',
|
|
1332
|
+
'vllm_orchestrator': 'orchestrator (fallback)',
|
|
1333
|
+
'vllm_reasoning': 'local reasoning (on-demand)',
|
|
1334
|
+
'vllm_coding': 'coding vLLM',
|
|
1335
|
+
'vllm_qwen': 'Qwen vLLM',
|
|
1336
|
+
'vllm_cloud_reasoning': 'cloud reasoning',
|
|
1337
|
+
'vllm_gemma4_reasoning': 'Gemma4 (cloud)',
|
|
1338
|
+
'vllm_gemma4_flagship': 'Gemma4 flagship (cloud)',
|
|
1339
|
+
'vllm_dgx': 'DGX Spark',
|
|
1340
|
+
'vllm_dgx_swap': 'DGX Spark (swap)',
|
|
1341
|
+
'vllm_dgx_embed': 'DGX Spark (embeddings)',
|
|
1342
|
+
'vllm_dgx_orch': 'DGX Spark (orchestrator)',
|
|
1343
|
+
'deepseek_api': 'DeepSeek API',
|
|
1344
|
+
'comfyui': 'ComfyUI',
|
|
1345
|
+
};
|
|
1346
|
+
// On-demand backends are expected to be offline — hide when down, show when up
|
|
1347
|
+
const onDemandBackends = new Set([
|
|
1348
|
+
'vllm_swap', 'vllm_reasoning', 'vllm_dgx_swap',
|
|
1349
|
+
'vllm_gemma4_reasoning', 'vllm_gemma4_flagship', 'vllm_cloud_reasoning',
|
|
1350
|
+
]);
|
|
1351
|
+
if (entries.length > 0) {
|
|
1352
|
+
const online = entries.filter(([, alive]) => alive);
|
|
1353
|
+
// Only show offline backends that aren't on-demand (expected off)
|
|
1354
|
+
const offline = entries.filter(([name, alive]) => !alive && !onDemandBackends.has(name));
|
|
1355
|
+
for (const [name, alive] of [...online, ...offline]) {
|
|
1356
|
+
const label = friendlyNames[name] || name.replace(/^vllm_/, '').replace(/_/g, ' ');
|
|
1357
|
+
const status = alive ? chalk.green('online') : chalk.red('offline');
|
|
1358
|
+
log(chalk.dim(` 🔧 ${label}: ${status}`));
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
case 'autonomous_start':
|
|
1364
|
+
case 'autonomous_task': {
|
|
1365
|
+
stopSpinner();
|
|
1366
|
+
const atTask = event.data.task || event.data.description || '';
|
|
1367
|
+
log(chalk.cyan(` 🤖 Autonomous task: ${atTask}`));
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1370
|
+
case 'expedition_created': {
|
|
1371
|
+
const expId = event.data.expedition_id || event.data.id || '?';
|
|
1372
|
+
log(chalk.cyan(` 🗺 Expedition created: ${expId}`));
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
case 'checkpoint_resumed': {
|
|
1376
|
+
const crTurn = event.data.turn || '?';
|
|
1377
|
+
log(chalk.dim(` ↩ Checkpoint resumed at turn ${crTurn}`));
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
case 'council_review': {
|
|
1381
|
+
const crVerdict = event.data.verdict || event.data.decision || '';
|
|
1382
|
+
const crReason = event.data.reason || '';
|
|
1383
|
+
log(chalk.magenta(` 👥 Council review: ${crVerdict}${crReason ? ` — ${crReason}` : ''}`));
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
case 'cuga_available': {
|
|
1387
|
+
break; // Internal signal, no display needed
|
|
1388
|
+
}
|
|
1389
|
+
case 'context_stage': {
|
|
1390
|
+
const csStage = event.data.substage || event.data.stage || 'context';
|
|
1391
|
+
const csStatus = event.data.status || event.data.detail || '';
|
|
1392
|
+
const csLabel = csStatus ? `${csStage}: ${csStatus}` : csStage;
|
|
1393
|
+
lastStage = csLabel;
|
|
1394
|
+
stopSpinner();
|
|
1395
|
+
log(chalk.dim(` [context_stage] ${csLabel}`));
|
|
1396
|
+
startSpinner(chalk.dim(csLabel));
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
case 'pipeline': {
|
|
1400
|
+
const plStage = event.data.stage || event.data.phase || '';
|
|
1401
|
+
const plStrategy = event.data.strategy || '';
|
|
1402
|
+
const plEffort = event.data.effort;
|
|
1403
|
+
// Show effort/strategy info inline
|
|
1404
|
+
if (plEffort && typeof plEffort === 'object') {
|
|
1405
|
+
const efLvl = plEffort.level || '?';
|
|
1406
|
+
const efLabel = plEffort.label || plEffort.tier || '';
|
|
1407
|
+
stopSpinner();
|
|
1408
|
+
log(chalk.dim(` [pipeline] effort=${efLvl}${efLabel ? ` (${efLabel})` : ''}${plStrategy ? ` strategy=${plStrategy}` : ''}`));
|
|
1409
|
+
}
|
|
1410
|
+
else if (plStage) {
|
|
1411
|
+
lastStage = plStage;
|
|
1412
|
+
if (spinner) {
|
|
1413
|
+
spinner.text = chalk.dim(plStage);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
case 'plan_complete':
|
|
1419
|
+
case 'plan_status': {
|
|
1420
|
+
const pcStatus = event.data.status || event.data.outcome || 'done';
|
|
1421
|
+
lastStage = `plan: ${pcStatus}`;
|
|
1422
|
+
log(chalk.dim(` [plan_complete] ${pcStatus}`));
|
|
1423
|
+
break;
|
|
1424
|
+
}
|
|
1425
|
+
case 'turn_progress': {
|
|
1426
|
+
const tpTurn = event.data.turn || '?';
|
|
1427
|
+
const tpMax = event.data.max_turns || '?';
|
|
1428
|
+
if (event.data.max_turns)
|
|
1429
|
+
lastKnownMaxTurns = event.data.max_turns;
|
|
1430
|
+
lastStage = `turn ${tpTurn}/${tpMax}`;
|
|
1431
|
+
if (spinner) {
|
|
1432
|
+
spinner.text = chalk.dim(`Turn ${tpTurn}/${tpMax}...`);
|
|
1433
|
+
}
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
default: {
|
|
1437
|
+
// Show content from unexpected events as fallback
|
|
1438
|
+
if (event.data.content && !hasOutput) {
|
|
1439
|
+
stopSpinner();
|
|
1440
|
+
process.stdout.write(event.data.content);
|
|
1441
|
+
content += event.data.content;
|
|
1442
|
+
hasOutput = true;
|
|
1443
|
+
}
|
|
1444
|
+
// Show meaningful unknown events so nothing is silently swallowed
|
|
1445
|
+
const msg = event.data.message || event.data.reason || event.data.status || '';
|
|
1446
|
+
if (msg && event.type !== 'debug') {
|
|
1447
|
+
log(chalk.dim(` [${event.type}] ${String(msg).slice(0, 120)}`));
|
|
1448
|
+
}
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
},
|
|
1453
|
+
getContent() {
|
|
1454
|
+
return content;
|
|
1455
|
+
},
|
|
1456
|
+
finish() {
|
|
1457
|
+
stopSpinner();
|
|
1458
|
+
if (!hasOutput) {
|
|
1459
|
+
console.log(chalk.dim(' (no response)'));
|
|
1460
|
+
}
|
|
1461
|
+
// ── Status bar — compact summary after each response ──
|
|
1462
|
+
const elapsed = Date.now() - startTime;
|
|
1463
|
+
const parts = [];
|
|
1464
|
+
if (traceAgent)
|
|
1465
|
+
parts.push(`agent: ${traceAgent}`);
|
|
1466
|
+
if (traceModel)
|
|
1467
|
+
parts.push(`model: ${traceModel}`);
|
|
1468
|
+
if (traceTurns > 0)
|
|
1469
|
+
parts.push(`turns: ${traceTurns}/${lastKnownMaxTurns}`);
|
|
1470
|
+
if (traceToolCalls.length > 0)
|
|
1471
|
+
parts.push(`tools: ${traceToolCalls.length}`);
|
|
1472
|
+
parts.push(`${(elapsed / 1000).toFixed(1)}s`);
|
|
1473
|
+
console.log(chalk.dim(` [${parts.join(' | ')}]`));
|
|
1474
|
+
// ── Persist session trace to disk ──
|
|
1475
|
+
if (sessionId) {
|
|
1476
|
+
try {
|
|
1477
|
+
const sessDir = join(homedir(), '.aither', 'sessions', sessionId);
|
|
1478
|
+
if (!existsSync(sessDir))
|
|
1479
|
+
mkdirSync(sessDir, { recursive: true });
|
|
1480
|
+
const profile = this.getSessionProfile();
|
|
1481
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1482
|
+
const filePath = join(sessDir, `${ts}.json`);
|
|
1483
|
+
writeFileSync(filePath, JSON.stringify(profile, null, 2));
|
|
1484
|
+
}
|
|
1485
|
+
catch (_e) {
|
|
1486
|
+
// best-effort — don't crash on trace write failure
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
1490
|
+
getTrace() {
|
|
1491
|
+
return traceEvents;
|
|
1492
|
+
},
|
|
1493
|
+
getSessionProfile() {
|
|
1494
|
+
return {
|
|
1495
|
+
session_id: sessionId || 'anonymous',
|
|
1496
|
+
prompt: prompt || '',
|
|
1497
|
+
started_at: startedAt,
|
|
1498
|
+
duration_ms: Date.now() - startTime,
|
|
1499
|
+
event_count: traceEvents.length,
|
|
1500
|
+
tool_calls: traceToolCalls,
|
|
1501
|
+
thinking_traces: traceThinking,
|
|
1502
|
+
context_sources: traceContextSources,
|
|
1503
|
+
model: traceModel,
|
|
1504
|
+
agent: traceAgent,
|
|
1505
|
+
errors: traceErrors,
|
|
1506
|
+
events: traceEvents,
|
|
1507
|
+
};
|
|
1508
|
+
},
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
/* ── Terminal Block Renderer ────────────────────────────────── */
|
|
1512
|
+
function renderTerminalBlocks(blocks) {
|
|
1513
|
+
for (const block of blocks) {
|
|
1514
|
+
switch (block.type) {
|
|
1515
|
+
case 'header':
|
|
1516
|
+
console.log(chalk.bold.cyan(`\n ${block.text}`) + (block.subtitle ? chalk.dim(` — ${block.subtitle}`) : ''));
|
|
1517
|
+
break;
|
|
1518
|
+
case 'kv': {
|
|
1519
|
+
const pairs = block.pairs || {};
|
|
1520
|
+
for (const [k, v] of Object.entries(pairs)) {
|
|
1521
|
+
console.log(chalk.dim(` ${k}: `) + chalk.white(String(v)));
|
|
1522
|
+
}
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
case 'table': {
|
|
1526
|
+
const cols = (block.columns || []).map((c) => typeof c === 'string' ? c : c.label);
|
|
1527
|
+
const rows = (block.rows || []).map((r) => r.map(String));
|
|
1528
|
+
if (cols.length > 0) {
|
|
1529
|
+
console.log(' ' + formatTable(cols, rows).split('\n').join('\n '));
|
|
1530
|
+
}
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
case 'callout': {
|
|
1534
|
+
const icons = { info: 'ℹ', success: '✓', warning: '⚠', error: '✗', tip: '💡' };
|
|
1535
|
+
const colors = {
|
|
1536
|
+
info: chalk.blue, success: chalk.green, warning: chalk.yellow,
|
|
1537
|
+
error: chalk.red, tip: chalk.magenta,
|
|
1538
|
+
};
|
|
1539
|
+
const color = colors[block.variant || 'info'] || chalk.blue;
|
|
1540
|
+
const icon = icons[block.variant || 'info'] || 'ℹ';
|
|
1541
|
+
console.log(color(` ${icon} ${block.title || ''} ${block.text}`));
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
case 'progress': {
|
|
1545
|
+
if (block.steps) {
|
|
1546
|
+
const icons = { done: '✓', active: '⟳', pending: '○', error: '✗' };
|
|
1547
|
+
for (const step of block.steps) {
|
|
1548
|
+
const icon = icons[step.status] || '○';
|
|
1549
|
+
const color = step.status === 'done' ? chalk.green : step.status === 'active' ? chalk.blue : step.status === 'error' ? chalk.red : chalk.dim;
|
|
1550
|
+
console.log(color(` ${icon} ${step.name}`) + (step.detail ? chalk.dim(` (${step.detail})`) : ''));
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
else if (block.value != null) {
|
|
1554
|
+
const pct = Math.round(block.value * 100);
|
|
1555
|
+
const filled = Math.round(pct / 5);
|
|
1556
|
+
const bar = '█'.repeat(filled) + '░'.repeat(20 - filled);
|
|
1557
|
+
console.log(chalk.blue(` [${bar}] ${pct}%`) + (block.label ? chalk.dim(` ${block.label}`) : ''));
|
|
1558
|
+
}
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
case 'scores': {
|
|
1562
|
+
for (const [name, val] of Object.entries(block.scores || {})) {
|
|
1563
|
+
const v = val;
|
|
1564
|
+
const pct = Math.round(v * 100);
|
|
1565
|
+
const color = pct >= 80 ? chalk.green : pct >= 50 ? chalk.yellow : chalk.red;
|
|
1566
|
+
console.log(` ${chalk.dim(name.padEnd(20))} ${color(`${pct}%`)}`);
|
|
1567
|
+
}
|
|
1568
|
+
break;
|
|
1569
|
+
}
|
|
1570
|
+
case 'code': {
|
|
1571
|
+
const label = block.filename || block.language || 'code';
|
|
1572
|
+
console.log(chalk.dim(`\n ─── ${label} ───`));
|
|
1573
|
+
const codeLines = (block.content || '').split('\n');
|
|
1574
|
+
for (const codeLine of codeLines) {
|
|
1575
|
+
console.log(chalk.white(` ${codeLine}`));
|
|
1576
|
+
}
|
|
1577
|
+
console.log(chalk.dim(` ─── end ───`));
|
|
1578
|
+
if (block.filename && block.filename !== 'code' && !block.filename.startsWith('code.')) {
|
|
1579
|
+
console.log(chalk.dim(` Saved to: ${block.filename}`));
|
|
1580
|
+
}
|
|
1581
|
+
break;
|
|
1582
|
+
}
|
|
1583
|
+
case 'list': {
|
|
1584
|
+
const items = block.items || [];
|
|
1585
|
+
for (const item of items) {
|
|
1586
|
+
const text = typeof item === 'string' ? item : item.text;
|
|
1587
|
+
console.log(chalk.dim(' • ') + text);
|
|
1588
|
+
}
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
case 'approve':
|
|
1592
|
+
console.log(chalk.yellow.bold(`\n ⚠ ${block.title}`));
|
|
1593
|
+
if (block.detail)
|
|
1594
|
+
console.log(chalk.dim(` ${block.detail}`));
|
|
1595
|
+
console.log(chalk.dim(` [${block.approve_label || 'Approve'}] / [${block.reject_label || 'Reject'}]`));
|
|
1596
|
+
console.log(chalk.dim(` (interactive approval — use web UI to respond)`));
|
|
1597
|
+
break;
|
|
1598
|
+
case 'form':
|
|
1599
|
+
if (block.title)
|
|
1600
|
+
console.log(chalk.cyan.bold(`\n 📋 ${block.title}`));
|
|
1601
|
+
if (block.description)
|
|
1602
|
+
console.log(chalk.dim(` ${block.description}`));
|
|
1603
|
+
for (const f of block.fields || []) {
|
|
1604
|
+
const def = f.default != null ? chalk.dim(` [${f.default}]`) : '';
|
|
1605
|
+
console.log(chalk.dim(` ${f.label}:`) + def);
|
|
1606
|
+
}
|
|
1607
|
+
console.log(chalk.dim(` (interactive form — use web UI to submit)`));
|
|
1608
|
+
break;
|
|
1609
|
+
case 'select':
|
|
1610
|
+
if (block.label)
|
|
1611
|
+
console.log(chalk.cyan(` ${block.label}`));
|
|
1612
|
+
for (const opt of block.options || []) {
|
|
1613
|
+
const label = typeof opt === 'string' ? opt : opt.label;
|
|
1614
|
+
const desc = typeof opt === 'object' && opt.description ? chalk.dim(` — ${opt.description}`) : '';
|
|
1615
|
+
console.log(chalk.dim(' ○ ') + label + desc);
|
|
1616
|
+
}
|
|
1617
|
+
console.log(chalk.dim(` (interactive selection — use web UI to choose)`));
|
|
1618
|
+
break;
|
|
1619
|
+
case 'markdown':
|
|
1620
|
+
if (block.text)
|
|
1621
|
+
console.log(' ' + renderMarkdown(block.text).trim().split('\n').join('\n '));
|
|
1622
|
+
break;
|
|
1623
|
+
default:
|
|
1624
|
+
// Unknown block — show type + any text content
|
|
1625
|
+
if (block.text || block.content) {
|
|
1626
|
+
console.log(chalk.dim(` [${block.type}] `) + (block.text || block.content));
|
|
1627
|
+
}
|
|
1628
|
+
break;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
/* ── Table Formatter ────────────────────────────────────────── */
|
|
1633
|
+
export function formatTable(headers, rows) {
|
|
1634
|
+
// Strip ANSI for width calculation
|
|
1635
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1636
|
+
const widths = headers.map((h, i) => Math.max(strip(h).length, ...rows.map(r => strip(r[i] || '').length)));
|
|
1637
|
+
const pad = (s, w) => s + ''.padEnd(Math.max(0, w - strip(s).length));
|
|
1638
|
+
const header = headers.map((h, i) => pad(h, widths[i])).join(' ');
|
|
1639
|
+
const sep = widths.map(w => '\u2500'.repeat(w)).join('\u2500\u2500');
|
|
1640
|
+
const body = rows.map(row => row.map((cell, i) => pad(cell || '', widths[i])).join(' ')).join('\n');
|
|
1641
|
+
return `${chalk.bold(header)}\n${chalk.dim(sep)}\n${body}`;
|
|
1642
|
+
}
|
|
1643
|
+
/* ── Steering Bar — dedicated input line during streaming ─────── */
|
|
1644
|
+
/**
|
|
1645
|
+
* Keeps a visible input line at the bottom of the terminal while
|
|
1646
|
+
* stream output prints above. Works on Windows Terminal (no ANSI
|
|
1647
|
+
* scroll regions).
|
|
1648
|
+
*
|
|
1649
|
+
* How it works:
|
|
1650
|
+
* 1. Intercepts process.stdout.write
|
|
1651
|
+
* 2. Before each write: erase the bar line so output doesn't collide
|
|
1652
|
+
* 3. After each write: redraw the bar on a fresh line below output
|
|
1653
|
+
* 4. User keystrokes update the bar's displayed text in real time
|
|
1654
|
+
* 5. On Enter, the bar clears and the repl routes to /chat/steer
|
|
1655
|
+
*
|
|
1656
|
+
* The bar line looks like:
|
|
1657
|
+
* ── [steer] > what the user is typing_
|
|
1658
|
+
*/
|
|
1659
|
+
export class SteeringBar {
|
|
1660
|
+
_active = false;
|
|
1661
|
+
_inputText = '';
|
|
1662
|
+
_statusText = '';
|
|
1663
|
+
_origWrite = null;
|
|
1664
|
+
_barDrawn = false;
|
|
1665
|
+
_debounceTimer = null;
|
|
1666
|
+
_spinFrame = 0;
|
|
1667
|
+
_spinTimer = null;
|
|
1668
|
+
static _SPIN_CHARS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
1669
|
+
get active() { return this._active; }
|
|
1670
|
+
get inputText() { return this._inputText; }
|
|
1671
|
+
activate() {
|
|
1672
|
+
if (this._active)
|
|
1673
|
+
return;
|
|
1674
|
+
this._active = true;
|
|
1675
|
+
this._inputText = '';
|
|
1676
|
+
this._statusText = '';
|
|
1677
|
+
this._barDrawn = false;
|
|
1678
|
+
if (!process.stdout.isTTY)
|
|
1679
|
+
return;
|
|
1680
|
+
const self = this;
|
|
1681
|
+
this._origWrite = process.stdout.write.bind(process.stdout);
|
|
1682
|
+
const origWrite = this._origWrite;
|
|
1683
|
+
process.stdout.write = function (chunk, encodingOrCb, cb) {
|
|
1684
|
+
if (!self._active) {
|
|
1685
|
+
return origWrite(chunk, encodingOrCb, cb);
|
|
1686
|
+
}
|
|
1687
|
+
// Erase bar before writing output
|
|
1688
|
+
if (self._barDrawn) {
|
|
1689
|
+
origWrite('\x1b8\x1b[J');
|
|
1690
|
+
self._barDrawn = false;
|
|
1691
|
+
}
|
|
1692
|
+
// Write the actual output
|
|
1693
|
+
const result = origWrite(chunk, encodingOrCb, cb);
|
|
1694
|
+
// Schedule bar redraw (debounced — token bursts produce many writes)
|
|
1695
|
+
if (self._debounceTimer)
|
|
1696
|
+
clearTimeout(self._debounceTimer);
|
|
1697
|
+
self._debounceTimer = setTimeout(() => {
|
|
1698
|
+
self._debounceTimer = null;
|
|
1699
|
+
if (self._active)
|
|
1700
|
+
self._drawBar(origWrite);
|
|
1701
|
+
}, 30);
|
|
1702
|
+
return result;
|
|
1703
|
+
};
|
|
1704
|
+
// Draw initial bar
|
|
1705
|
+
this._drawBar(origWrite);
|
|
1706
|
+
}
|
|
1707
|
+
deactivate() {
|
|
1708
|
+
if (!this._active)
|
|
1709
|
+
return;
|
|
1710
|
+
this._active = false;
|
|
1711
|
+
this._statusText = '';
|
|
1712
|
+
if (this._debounceTimer) {
|
|
1713
|
+
clearTimeout(this._debounceTimer);
|
|
1714
|
+
this._debounceTimer = null;
|
|
1715
|
+
}
|
|
1716
|
+
if (this._spinTimer) {
|
|
1717
|
+
clearInterval(this._spinTimer);
|
|
1718
|
+
this._spinTimer = null;
|
|
1719
|
+
}
|
|
1720
|
+
// Erase bar if drawn
|
|
1721
|
+
if (this._barDrawn && this._origWrite) {
|
|
1722
|
+
this._origWrite('\x1b8\x1b[J');
|
|
1723
|
+
this._barDrawn = false;
|
|
1724
|
+
}
|
|
1725
|
+
// Restore stdout.write
|
|
1726
|
+
if (this._origWrite) {
|
|
1727
|
+
process.stdout.write = this._origWrite;
|
|
1728
|
+
this._origWrite = null;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
/** Schedule a single debounced redraw. All state changes route here. */
|
|
1732
|
+
_scheduleRedraw() {
|
|
1733
|
+
if (!this._active || !this._origWrite)
|
|
1734
|
+
return;
|
|
1735
|
+
if (this._debounceTimer)
|
|
1736
|
+
clearTimeout(this._debounceTimer);
|
|
1737
|
+
const w = this._origWrite;
|
|
1738
|
+
this._debounceTimer = setTimeout(() => {
|
|
1739
|
+
this._debounceTimer = null;
|
|
1740
|
+
if (!this._active)
|
|
1741
|
+
return;
|
|
1742
|
+
if (this._barDrawn) {
|
|
1743
|
+
w('\x1b8\x1b[J');
|
|
1744
|
+
this._barDrawn = false;
|
|
1745
|
+
}
|
|
1746
|
+
this._drawBar(w);
|
|
1747
|
+
}, 16);
|
|
1748
|
+
}
|
|
1749
|
+
/** Show spinner + status text in the bar (replaces ora when bar is active). */
|
|
1750
|
+
setStatus(text) {
|
|
1751
|
+
this._statusText = text;
|
|
1752
|
+
if (!this._spinTimer && this._active && this._origWrite) {
|
|
1753
|
+
const origWrite = this._origWrite;
|
|
1754
|
+
this._spinTimer = setInterval(() => {
|
|
1755
|
+
this._spinFrame = (this._spinFrame + 1) % SteeringBar._SPIN_CHARS.length;
|
|
1756
|
+
if (this._active)
|
|
1757
|
+
this._scheduleRedraw();
|
|
1758
|
+
}, 80);
|
|
1759
|
+
}
|
|
1760
|
+
// Don't redraw immediately — the interceptor or spin timer will do it
|
|
1761
|
+
}
|
|
1762
|
+
/** Clear status text. */
|
|
1763
|
+
clearStatus() {
|
|
1764
|
+
this._statusText = '';
|
|
1765
|
+
if (this._spinTimer) {
|
|
1766
|
+
clearInterval(this._spinTimer);
|
|
1767
|
+
this._spinTimer = null;
|
|
1768
|
+
}
|
|
1769
|
+
// Don't redraw — next stdout write or setStatus will handle it
|
|
1770
|
+
}
|
|
1771
|
+
/** Update displayed input text (called from readline _ttyWrite hook). */
|
|
1772
|
+
setInput(text) {
|
|
1773
|
+
this._inputText = text;
|
|
1774
|
+
if (this._active && this._origWrite) {
|
|
1775
|
+
// Keystroke — redraw immediately (no debounce for input responsiveness)
|
|
1776
|
+
if (this._debounceTimer)
|
|
1777
|
+
clearTimeout(this._debounceTimer);
|
|
1778
|
+
this._debounceTimer = null;
|
|
1779
|
+
if (this._barDrawn) {
|
|
1780
|
+
this._origWrite('\x1b8\x1b[J');
|
|
1781
|
+
this._barDrawn = false;
|
|
1782
|
+
}
|
|
1783
|
+
this._drawBar(this._origWrite);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/** Clear input after steer send. */
|
|
1787
|
+
clearInput() {
|
|
1788
|
+
this._inputText = '';
|
|
1789
|
+
this._scheduleRedraw();
|
|
1790
|
+
}
|
|
1791
|
+
_drawBar(write) {
|
|
1792
|
+
const cols = process.stdout.columns || 80;
|
|
1793
|
+
const barWidth = Math.min(cols, 60);
|
|
1794
|
+
const sep = chalk.dim('─'.repeat(barWidth));
|
|
1795
|
+
// Status line (spinner + status text)
|
|
1796
|
+
let statusLine = '';
|
|
1797
|
+
if (this._statusText) {
|
|
1798
|
+
const frame = SteeringBar._SPIN_CHARS[this._spinFrame];
|
|
1799
|
+
statusLine = `\n${chalk.cyan(frame)} ${chalk.dim(this._statusText)}\x1b[K`;
|
|
1800
|
+
}
|
|
1801
|
+
// Input prompt
|
|
1802
|
+
const prefix = chalk.dim(' [steer]') + chalk.yellow(' > ');
|
|
1803
|
+
const prefixLen = 11;
|
|
1804
|
+
const maxInput = cols - prefixLen - 1;
|
|
1805
|
+
const display = this._inputText.length > maxInput
|
|
1806
|
+
? this._inputText.slice(-maxInput)
|
|
1807
|
+
: this._inputText;
|
|
1808
|
+
write('\x1b7'); // Save cursor position (end of content)
|
|
1809
|
+
write(`${statusLine}\n${sep}\n${prefix}${display}\x1b[K`);
|
|
1810
|
+
this._barDrawn = true;
|
|
1811
|
+
}
|
|
1812
|
+
}
|