upd
This commit is contained in:
106
src/fido.js
106
src/fido.js
@@ -22,13 +22,13 @@
|
||||
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { editor, input, select } from '@inquirer/prompts';
|
||||
import { editor, input, select, confirm } from '@inquirer/prompts';
|
||||
import { marked } from 'marked';
|
||||
import { markedTerminal } from 'marked-terminal';
|
||||
|
||||
import { LLMOrbiter, OpenRouterProvider } from 'llmorbiter';
|
||||
|
||||
import { API_KEY, MODEL, UNDO_FILE, SUSPEND_DIR, ROOT_DIR } from './config.js';
|
||||
import { API_KEY, MODEL, UNDO_FILE, SUSPEND_DIR, ROOT_DIR, COMPACT_DEBUG } from './config.js';
|
||||
import { listDir } from './dir-listing.js';
|
||||
import { loadUndoLog, getUndoLog } from './undo.js';
|
||||
import { loadSuspends, addSuspend, getSuspends } from './suspend.js';
|
||||
@@ -36,7 +36,7 @@ import { tools, toolCallNotifications, toolResultNotifications } from './tools/i
|
||||
import { printToolCall, emitGenericToolResult } from './tools/notify.js';
|
||||
import { autocompleteInput } from './autocomplete-input.js';
|
||||
import { COMMANDS, findCommand } from './slash-commands.js';
|
||||
import { compactPatchToolCalls } from './compact.js';
|
||||
import { compactPatchToolCalls, invalidateStaleReadResults } from './compact.js';
|
||||
|
||||
// ─── Markdown → console renderer ───────────────────────────────────────────────
|
||||
|
||||
@@ -221,11 +221,23 @@ function printToolError(name, error) {
|
||||
console.log(chalk.red(` ✖ Tool error [${name}]: ${error}`));
|
||||
}
|
||||
|
||||
function printUsage(u) {
|
||||
function printUsage(u, totals) {
|
||||
const parts = [`in=${u.input}`, `out=${u.output}`];
|
||||
if (u.cached) parts.push(`cached=${u.cached}`);
|
||||
if (u.cost) parts.push(`cost=$${u.cost.toFixed(5)}`);
|
||||
console.log(chalk.dim(` 📊 ${parts.join(' ')}`));
|
||||
let line = ` 📊 ${parts.join(' ')}`;
|
||||
if (totals) {
|
||||
totals.calls += 1;
|
||||
totals.input += u.input || 0;
|
||||
totals.output += u.output || 0;
|
||||
totals.cached += u.cached || 0;
|
||||
totals.cost += u.cost || 0;
|
||||
const acc = [`in=${totals.input}`, `out=${totals.output}`];
|
||||
if (totals.cached) acc.push(`cached=${totals.cached}`);
|
||||
if (totals.cost) acc.push(`cost=$${totals.cost.toFixed(5)}`);
|
||||
line += ` [(${totals.calls}) ${acc.join(', ')}]`;
|
||||
}
|
||||
console.log(chalk.dim(line));
|
||||
}
|
||||
|
||||
function formatWhen(iso) {
|
||||
@@ -271,10 +283,20 @@ async function main() {
|
||||
loadSuspends();
|
||||
printBanner();
|
||||
|
||||
// Most recent reasoning the model streamed during the active turn. The
|
||||
// provider reports reasoning cumulatively, so we keep the latest value. If a
|
||||
// turn is cancelled mid-flight this is offered back to the user so it can be
|
||||
// preserved in the transcript instead of being discarded.
|
||||
let turnReasoning = '';
|
||||
|
||||
const provider = new OpenRouterProvider({ apiKey: API_KEY });
|
||||
|
||||
// Running totals across the session, echoed after each per-call usage line.
|
||||
const usageTotals = { calls: 0, input: 0, output: 0, cached: 0, cost: 0 };
|
||||
|
||||
const orbiter = new LLMOrbiter(provider, {
|
||||
onStatus: (s) => printStatus(s),
|
||||
onReasoning: (text) => { turnReasoning = text; },
|
||||
onToolCall: (c) => {
|
||||
const notify = toolCallNotifications[c.name];
|
||||
if (notify) notify(c.args);
|
||||
@@ -287,20 +309,26 @@ async function main() {
|
||||
printToolResult(r.name, r.result);
|
||||
},
|
||||
onToolError: (e) => printToolError(e.name, e.error),
|
||||
onUsage: (u) => printUsage(u),
|
||||
onUsage: (u) => printUsage(u, usageTotals),
|
||||
});
|
||||
|
||||
// Conversation state: persisted across turns
|
||||
const systemPrompt = buildSystemPrompt();
|
||||
let messages = [{ role: 'system', content: systemPrompt }];
|
||||
|
||||
console.log(chalk.dim(' Type your request below. Type / for commands, /paste for multiline. Press Ctrl+C to exit.\n'));
|
||||
console.log(chalk.dim(' Type your request below. Type / for commands, /paste for multiline.'));
|
||||
console.log(chalk.dim(' Ctrl+C exits at the prompt; while the model is running it cancels the turn.\n'));
|
||||
|
||||
// Restored into the prompt after a Ctrl+C cancels a running turn, so the
|
||||
// message can be edited and resent instead of retyped.
|
||||
let draft = '';
|
||||
|
||||
while (true) {
|
||||
let userInput;
|
||||
try {
|
||||
userInput = await autocompleteInput({
|
||||
message: chalk.bold.cyan('You'),
|
||||
default: draft,
|
||||
theme: {
|
||||
prefix: chalk.cyan('›'),
|
||||
style: {
|
||||
@@ -314,6 +342,7 @@ async function main() {
|
||||
console.log(chalk.dim('\n Goodbye! 🐕\n'));
|
||||
break;
|
||||
}
|
||||
draft = '';
|
||||
|
||||
if (!userInput.trim()) continue;
|
||||
|
||||
@@ -436,6 +465,15 @@ async function main() {
|
||||
|
||||
console.log('');
|
||||
|
||||
// No prompt owns the terminal while the model works, so a Ctrl+C would
|
||||
// otherwise hit Node's default handler and kill the process. Catch it
|
||||
// here instead: abort the active run and fall back through to the prompt
|
||||
// (prefilled with the message) rather than exiting.
|
||||
// Fresh reasoning buffer for this turn.
|
||||
turnReasoning = '';
|
||||
|
||||
const onSigint = () => orbiter.abort('cancelled by user');
|
||||
process.on('SIGINT', onSigint);
|
||||
try {
|
||||
const result = await orbiter.run({
|
||||
prompt: userInput,
|
||||
@@ -450,11 +488,61 @@ async function main() {
|
||||
// Token savings: the patch tool's arguments carry whole file bodies
|
||||
// (search/replace). Once applied they are dead weight, so shrink each
|
||||
// patch tool call in the transcript to a head/tail excerpt.
|
||||
compactPatchToolCalls(messages);
|
||||
const compactedPatches = compactPatchToolCalls(messages);
|
||||
// And the other half of the same problem: a read_file result carries a
|
||||
// whole file body that a later patch made stale. Replace it with a
|
||||
// short "file was overwritten meanwhile" marker so the old contents are
|
||||
// not re-sent every turn (nor mistaken for the current file).
|
||||
const invalidatedReads = invalidateStaleReadResults(messages);
|
||||
if (COMPACT_DEBUG) {
|
||||
console.log(chalk.dim(
|
||||
` [compact] patch tool calls compacted: ${compactedPatches} · ` +
|
||||
`stale read results invalidated: ${invalidatedReads}`
|
||||
));
|
||||
}
|
||||
|
||||
printAssistant(result.content);
|
||||
} catch (err) {
|
||||
console.log(chalk.red(`\n ✖ Error: ${err.message}\n`));
|
||||
if (err && err.name === 'AbortError') {
|
||||
// Ctrl+C cancelled the run — put the message back in the prompt
|
||||
// so it can be edited and resent. The aborted transcript is
|
||||
// discarded, so `messages` is untouched and the turn is not lost.
|
||||
draft = userInput;
|
||||
console.log(chalk.dim('\n ⏹ Cancelled — your message is back in the prompt; edit it and press Enter to resend.\n'));
|
||||
|
||||
// If the model already produced reasoning before the cancel,
|
||||
// offer to keep it. Kept reasoning is added to the transcript
|
||||
// exactly like normal reasoning (an assistant message carrying
|
||||
// the reasoning text), tagged with a marker at the end so it's
|
||||
// clear the turn was cut short.
|
||||
const reasoning = turnReasoning.trim();
|
||||
if (reasoning) {
|
||||
let keep = false;
|
||||
try {
|
||||
keep = await confirm({
|
||||
message: chalk.bold.cyan('Keep the reasoning the model produced before cancelling?'),
|
||||
default: true,
|
||||
theme: {
|
||||
prefix: chalk.cyan('›'),
|
||||
style: { message: chalk.bold.cyan },
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
keep = false; // prompt dismissed (e.g. Ctrl+C) — don't keep
|
||||
}
|
||||
if (keep) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: `${reasoning}\n---USER-CANCELLED---`,
|
||||
});
|
||||
console.log(chalk.dim(' ↗ Reasoning kept in the transcript.\n'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.red(`\n ✖ Error: ${err.message}\n`));
|
||||
}
|
||||
} finally {
|
||||
process.removeListener('SIGINT', onSigint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user