@quantish/agent 0.1.19 → 0.1.21
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/index.js +507 -47
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3261,17 +3261,38 @@ var OpenRouterProvider = class {
|
|
|
3261
3261
|
const toolCalls = [];
|
|
3262
3262
|
for (const [, tc] of toolCallsInProgress) {
|
|
3263
3263
|
try {
|
|
3264
|
+
if (!tc || !tc.name) {
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3264
3267
|
let args = tc.arguments?.trim() || "{}";
|
|
3265
|
-
if (args && !args.endsWith("}")) {
|
|
3266
|
-
|
|
3268
|
+
if (args && !args.endsWith("}") && !args.endsWith("]")) {
|
|
3269
|
+
const openBraces = (args.match(/{/g) || []).length;
|
|
3270
|
+
const closeBraces = (args.match(/}/g) || []).length;
|
|
3271
|
+
if (openBraces > closeBraces) {
|
|
3272
|
+
args = args + "}".repeat(openBraces - closeBraces);
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
if (!args || args === "" || args === "undefined") {
|
|
3276
|
+
args = "{}";
|
|
3267
3277
|
}
|
|
3268
3278
|
const input = JSON.parse(args);
|
|
3269
|
-
|
|
3270
|
-
|
|
3279
|
+
const toolId = tc.id || `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
3280
|
+
toolCalls.push({ id: toolId, name: tc.name, input });
|
|
3281
|
+
callbacks.onToolCall?.(toolId, tc.name, input);
|
|
3271
3282
|
} catch (e) {
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3283
|
+
const toolName = tc?.name || "unknown_tool";
|
|
3284
|
+
let parsedInput = {};
|
|
3285
|
+
try {
|
|
3286
|
+
const argsStr = tc?.arguments || "";
|
|
3287
|
+
const matches = argsStr.matchAll(/(\w+):\s*"([^"]+)"/g);
|
|
3288
|
+
for (const match of matches) {
|
|
3289
|
+
parsedInput[match[1]] = match[2];
|
|
3290
|
+
}
|
|
3291
|
+
} catch {
|
|
3292
|
+
}
|
|
3293
|
+
const toolId = tc?.id || `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
3294
|
+
toolCalls.push({ id: toolId, name: toolName, input: parsedInput });
|
|
3295
|
+
callbacks.onToolCall?.(toolId, toolName, parsedInput);
|
|
3275
3296
|
}
|
|
3276
3297
|
}
|
|
3277
3298
|
const cost = calculateOpenRouterCost(
|
|
@@ -3486,31 +3507,40 @@ function extractTokenInfo(token) {
|
|
|
3486
3507
|
}
|
|
3487
3508
|
var DEFAULT_SYSTEM_PROMPT = `You are Quantish, an AI coding and trading agent.
|
|
3488
3509
|
|
|
3489
|
-
You have two sets of capabilities:
|
|
3490
|
-
|
|
3491
3510
|
## Trading Tools (via MCP)
|
|
3492
|
-
You can interact with Polymarket prediction markets:
|
|
3493
3511
|
- Check wallet balances and positions
|
|
3494
3512
|
- Place, cancel, and manage orders
|
|
3495
|
-
- Transfer funds and claim winnings
|
|
3496
3513
|
- Get market prices and orderbook data
|
|
3514
|
+
- Search markets on Polymarket, Kalshi, Limitless
|
|
3497
3515
|
|
|
3498
3516
|
## Coding Tools (local)
|
|
3499
|
-
|
|
3500
|
-
-
|
|
3501
|
-
-
|
|
3502
|
-
-
|
|
3503
|
-
- Use git for version control
|
|
3517
|
+
- read_file, write_file, edit_file, list_dir
|
|
3518
|
+
- grep (search), find_files
|
|
3519
|
+
- run_command (blocking), start_background_process (non-blocking)
|
|
3520
|
+
- git operations
|
|
3504
3521
|
|
|
3505
|
-
##
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
-
|
|
3509
|
-
-
|
|
3510
|
-
-
|
|
3511
|
-
-
|
|
3522
|
+
## IMPORTANT: Background vs Blocking Commands
|
|
3523
|
+
|
|
3524
|
+
Use \`start_background_process\` for:
|
|
3525
|
+
- Dev servers: npm start, npm run dev, yarn dev, vite, next dev
|
|
3526
|
+
- Watch mode: npm run watch, tsc --watch
|
|
3527
|
+
- Any server or long-running process
|
|
3528
|
+
- Returns immediately with a process ID
|
|
3512
3529
|
|
|
3513
|
-
|
|
3530
|
+
Use \`run_command\` for:
|
|
3531
|
+
- Quick commands: ls, cat, npm install, npm run build
|
|
3532
|
+
- One-time operations that complete quickly
|
|
3533
|
+
- Blocks until command finishes
|
|
3534
|
+
|
|
3535
|
+
After starting a background process:
|
|
3536
|
+
1. Use \`get_process_output(process_id)\` to check if it started correctly
|
|
3537
|
+
2. Use \`list_processes()\` to see all running processes
|
|
3538
|
+
3. Use \`stop_process(process_id)\` to stop when done
|
|
3539
|
+
|
|
3540
|
+
## Guidelines
|
|
3541
|
+
- Be concise
|
|
3542
|
+
- Prices on Polymarket are 0.01-0.99 (probabilities)
|
|
3543
|
+
- For dangerous operations (rm, sudo), explain first`;
|
|
3514
3544
|
var Agent = class {
|
|
3515
3545
|
anthropic;
|
|
3516
3546
|
llmProvider;
|
|
@@ -4157,6 +4187,18 @@ ${userMessage}`;
|
|
|
4157
4187
|
setHistory(history) {
|
|
4158
4188
|
this.conversationHistory = history;
|
|
4159
4189
|
}
|
|
4190
|
+
/**
|
|
4191
|
+
* Get conversation history (alias for getHistory)
|
|
4192
|
+
*/
|
|
4193
|
+
getConversationHistory() {
|
|
4194
|
+
return this.getHistory();
|
|
4195
|
+
}
|
|
4196
|
+
/**
|
|
4197
|
+
* Set conversation history (alias for setHistory)
|
|
4198
|
+
*/
|
|
4199
|
+
setConversationHistory(history) {
|
|
4200
|
+
this.setHistory(history);
|
|
4201
|
+
}
|
|
4160
4202
|
};
|
|
4161
4203
|
function createAgent(config) {
|
|
4162
4204
|
return new Agent(config);
|
|
@@ -4215,10 +4257,204 @@ function tableRow(label, value, width = 20) {
|
|
|
4215
4257
|
}
|
|
4216
4258
|
|
|
4217
4259
|
// src/ui/App.tsx
|
|
4218
|
-
import { useState, useCallback, useRef, useEffect } from "react";
|
|
4260
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
|
4219
4261
|
import { Box, Text, useApp, useInput } from "ink";
|
|
4220
4262
|
import TextInput from "ink-text-input";
|
|
4221
4263
|
import Spinner from "ink-spinner";
|
|
4264
|
+
|
|
4265
|
+
// src/config/sessions.ts
|
|
4266
|
+
import { homedir as homedir2 } from "os";
|
|
4267
|
+
import { join as join3 } from "path";
|
|
4268
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, unlinkSync, writeFileSync, readFileSync } from "fs";
|
|
4269
|
+
var SESSIONS_DIR = join3(homedir2(), ".quantish", "sessions");
|
|
4270
|
+
var INDEX_FILE = join3(SESSIONS_DIR, "index.json");
|
|
4271
|
+
function ensureSessionsDir() {
|
|
4272
|
+
if (!existsSync2(SESSIONS_DIR)) {
|
|
4273
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
function generateSessionId() {
|
|
4277
|
+
const timestamp = Date.now().toString(36);
|
|
4278
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
4279
|
+
return `${timestamp}-${random}`;
|
|
4280
|
+
}
|
|
4281
|
+
function loadIndex() {
|
|
4282
|
+
ensureSessionsDir();
|
|
4283
|
+
if (!existsSync2(INDEX_FILE)) {
|
|
4284
|
+
return { lastSessionId: null, sessions: [] };
|
|
4285
|
+
}
|
|
4286
|
+
try {
|
|
4287
|
+
const data = readFileSync(INDEX_FILE, "utf-8");
|
|
4288
|
+
return JSON.parse(data);
|
|
4289
|
+
} catch {
|
|
4290
|
+
return { lastSessionId: null, sessions: [] };
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
function saveIndex(index) {
|
|
4294
|
+
ensureSessionsDir();
|
|
4295
|
+
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), "utf-8");
|
|
4296
|
+
}
|
|
4297
|
+
function getSessionPath(id) {
|
|
4298
|
+
return join3(SESSIONS_DIR, `${id}.json`);
|
|
4299
|
+
}
|
|
4300
|
+
var SessionManager = class {
|
|
4301
|
+
/**
|
|
4302
|
+
* Save a new session or update an existing one
|
|
4303
|
+
*/
|
|
4304
|
+
saveSession(messages, model, provider, name, existingId) {
|
|
4305
|
+
ensureSessionsDir();
|
|
4306
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4307
|
+
const id = existingId || generateSessionId();
|
|
4308
|
+
const sessionName = name || `Session ${(/* @__PURE__ */ new Date()).toLocaleDateString()} ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
|
|
4309
|
+
const session = {
|
|
4310
|
+
id,
|
|
4311
|
+
name: sessionName,
|
|
4312
|
+
createdAt: existingId ? this.getSession(existingId)?.createdAt || now : now,
|
|
4313
|
+
updatedAt: now,
|
|
4314
|
+
messages,
|
|
4315
|
+
model,
|
|
4316
|
+
provider,
|
|
4317
|
+
tokenCount: this.estimateTokenCount(messages)
|
|
4318
|
+
};
|
|
4319
|
+
const sessionPath = getSessionPath(id);
|
|
4320
|
+
writeFileSync(sessionPath, JSON.stringify(session, null, 2), "utf-8");
|
|
4321
|
+
const index = loadIndex();
|
|
4322
|
+
index.lastSessionId = id;
|
|
4323
|
+
const existingIndex = index.sessions.findIndex((s) => s.id === id);
|
|
4324
|
+
const sessionMeta = {
|
|
4325
|
+
id,
|
|
4326
|
+
name: sessionName,
|
|
4327
|
+
createdAt: session.createdAt,
|
|
4328
|
+
updatedAt: now,
|
|
4329
|
+
messageCount: messages.length
|
|
4330
|
+
};
|
|
4331
|
+
if (existingIndex >= 0) {
|
|
4332
|
+
index.sessions[existingIndex] = sessionMeta;
|
|
4333
|
+
} else {
|
|
4334
|
+
index.sessions.unshift(sessionMeta);
|
|
4335
|
+
}
|
|
4336
|
+
if (index.sessions.length > 50) {
|
|
4337
|
+
const toRemove = index.sessions.splice(50);
|
|
4338
|
+
for (const s of toRemove) {
|
|
4339
|
+
try {
|
|
4340
|
+
unlinkSync(getSessionPath(s.id));
|
|
4341
|
+
} catch {
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
saveIndex(index);
|
|
4346
|
+
return session;
|
|
4347
|
+
}
|
|
4348
|
+
/**
|
|
4349
|
+
* Get a session by ID
|
|
4350
|
+
*/
|
|
4351
|
+
getSession(id) {
|
|
4352
|
+
const sessionPath = getSessionPath(id);
|
|
4353
|
+
if (!existsSync2(sessionPath)) {
|
|
4354
|
+
return null;
|
|
4355
|
+
}
|
|
4356
|
+
try {
|
|
4357
|
+
const data = readFileSync(sessionPath, "utf-8");
|
|
4358
|
+
return JSON.parse(data);
|
|
4359
|
+
} catch {
|
|
4360
|
+
return null;
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
/**
|
|
4364
|
+
* Get the last session
|
|
4365
|
+
*/
|
|
4366
|
+
getLastSession() {
|
|
4367
|
+
const index = loadIndex();
|
|
4368
|
+
if (!index.lastSessionId) {
|
|
4369
|
+
return null;
|
|
4370
|
+
}
|
|
4371
|
+
return this.getSession(index.lastSessionId);
|
|
4372
|
+
}
|
|
4373
|
+
/**
|
|
4374
|
+
* Get a session by name
|
|
4375
|
+
*/
|
|
4376
|
+
getSessionByName(name) {
|
|
4377
|
+
const index = loadIndex();
|
|
4378
|
+
const session = index.sessions.find(
|
|
4379
|
+
(s) => s.name.toLowerCase() === name.toLowerCase()
|
|
4380
|
+
);
|
|
4381
|
+
if (!session) {
|
|
4382
|
+
return null;
|
|
4383
|
+
}
|
|
4384
|
+
return this.getSession(session.id);
|
|
4385
|
+
}
|
|
4386
|
+
/**
|
|
4387
|
+
* List all sessions
|
|
4388
|
+
*/
|
|
4389
|
+
listSessions() {
|
|
4390
|
+
const index = loadIndex();
|
|
4391
|
+
return index.sessions;
|
|
4392
|
+
}
|
|
4393
|
+
/**
|
|
4394
|
+
* Delete a session
|
|
4395
|
+
*/
|
|
4396
|
+
deleteSession(id) {
|
|
4397
|
+
const sessionPath = getSessionPath(id);
|
|
4398
|
+
if (!existsSync2(sessionPath)) {
|
|
4399
|
+
return false;
|
|
4400
|
+
}
|
|
4401
|
+
try {
|
|
4402
|
+
unlinkSync(sessionPath);
|
|
4403
|
+
const index = loadIndex();
|
|
4404
|
+
index.sessions = index.sessions.filter((s) => s.id !== id);
|
|
4405
|
+
if (index.lastSessionId === id) {
|
|
4406
|
+
index.lastSessionId = index.sessions[0]?.id || null;
|
|
4407
|
+
}
|
|
4408
|
+
saveIndex(index);
|
|
4409
|
+
return true;
|
|
4410
|
+
} catch {
|
|
4411
|
+
return false;
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
/**
|
|
4415
|
+
* Clear all sessions
|
|
4416
|
+
*/
|
|
4417
|
+
clearAllSessions() {
|
|
4418
|
+
ensureSessionsDir();
|
|
4419
|
+
try {
|
|
4420
|
+
const files = readdirSync(SESSIONS_DIR);
|
|
4421
|
+
for (const file of files) {
|
|
4422
|
+
try {
|
|
4423
|
+
unlinkSync(join3(SESSIONS_DIR, file));
|
|
4424
|
+
} catch {
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
} catch {
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
/**
|
|
4431
|
+
* Estimate token count from messages (rough estimate)
|
|
4432
|
+
*/
|
|
4433
|
+
estimateTokenCount(messages) {
|
|
4434
|
+
let totalChars = 0;
|
|
4435
|
+
for (const msg of messages) {
|
|
4436
|
+
if (typeof msg.content === "string") {
|
|
4437
|
+
totalChars += msg.content.length;
|
|
4438
|
+
} else if (Array.isArray(msg.content)) {
|
|
4439
|
+
for (const block of msg.content) {
|
|
4440
|
+
if ("text" in block && typeof block.text === "string") {
|
|
4441
|
+
totalChars += block.text.length;
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
return Math.ceil(totalChars / 4);
|
|
4447
|
+
}
|
|
4448
|
+
};
|
|
4449
|
+
var sessionManager = null;
|
|
4450
|
+
function getSessionManager() {
|
|
4451
|
+
if (!sessionManager) {
|
|
4452
|
+
sessionManager = new SessionManager();
|
|
4453
|
+
}
|
|
4454
|
+
return sessionManager;
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4457
|
+
// src/ui/App.tsx
|
|
4222
4458
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4223
4459
|
function formatTokenCount(count) {
|
|
4224
4460
|
if (count < 1e3) return String(count);
|
|
@@ -4270,6 +4506,10 @@ function formatResult(result, maxLength = 200) {
|
|
|
4270
4506
|
}
|
|
4271
4507
|
return String(result);
|
|
4272
4508
|
}
|
|
4509
|
+
function cleanModelOutput(text) {
|
|
4510
|
+
if (!text) return text;
|
|
4511
|
+
return text.replace(/<tool_call>/g, "").replace(/<\/tool_call>/g, "").replace(/<arg_key>/g, "").replace(/<\/arg_key>/g, "").replace(/<function_call>/g, "").replace(/<\/function_call>/g, "").trim();
|
|
4512
|
+
}
|
|
4273
4513
|
function App({ agent, onExit }) {
|
|
4274
4514
|
const { exit } = useApp();
|
|
4275
4515
|
const [messages, setMessages] = useState([]);
|
|
@@ -4291,6 +4531,10 @@ function App({ agent, onExit }) {
|
|
|
4291
4531
|
});
|
|
4292
4532
|
const completedToolCalls = useRef([]);
|
|
4293
4533
|
const abortController = useRef(null);
|
|
4534
|
+
const [queuedInput, setQueuedInput] = useState("");
|
|
4535
|
+
const [hasQueuedMessage, setHasQueuedMessage] = useState(false);
|
|
4536
|
+
const sessionManager2 = useMemo(() => getSessionManager(), []);
|
|
4537
|
+
const [currentSessionId, setCurrentSessionId] = useState(null);
|
|
4294
4538
|
const handleSlashCommand = useCallback((command) => {
|
|
4295
4539
|
const cmd = command.slice(1).toLowerCase().split(" ")[0];
|
|
4296
4540
|
const args = command.slice(cmd.length + 2).trim();
|
|
@@ -4301,20 +4545,34 @@ function App({ agent, onExit }) {
|
|
|
4301
4545
|
content: `\u{1F4DA} Available Commands:
|
|
4302
4546
|
/clear - Clear conversation history
|
|
4303
4547
|
/compact - Summarize conversation (keeps context, saves tokens)
|
|
4304
|
-
/model - Switch model (opus, sonnet, haiku, minimax,
|
|
4548
|
+
/model - Switch model (opus, sonnet, haiku, minimax, etc.)
|
|
4305
4549
|
/provider - Switch LLM provider (anthropic, openrouter)
|
|
4306
4550
|
/cost - Show session cost breakdown
|
|
4307
4551
|
/help - Show this help message
|
|
4308
4552
|
/tools - List available tools
|
|
4309
4553
|
/config - Show configuration info
|
|
4554
|
+
|
|
4555
|
+
\u{1F5C2}\uFE0F Session Commands:
|
|
4556
|
+
/save [name] - Save current session
|
|
4557
|
+
/resume - Resume last session
|
|
4558
|
+
/sessions - List saved sessions
|
|
4559
|
+
/load <id> - Load a saved session
|
|
4560
|
+
/forget - Delete all saved sessions
|
|
4561
|
+
|
|
4562
|
+
\u{1F4CB} Process Commands:
|
|
4310
4563
|
/processes - List running background processes
|
|
4311
4564
|
/stop <id> - Stop a background process by ID
|
|
4312
4565
|
/stopall - Stop all background processes
|
|
4566
|
+
|
|
4313
4567
|
/exit - Exit the CLI
|
|
4314
4568
|
|
|
4315
4569
|
\u2328\uFE0F Keyboard Shortcuts:
|
|
4316
|
-
Esc - Interrupt current generation
|
|
4317
|
-
|
|
4570
|
+
Esc - Interrupt current generation (or send queued message)
|
|
4571
|
+
Enter - Queue message while agent is working
|
|
4572
|
+
Ctrl+C - Exit (stops all processes)
|
|
4573
|
+
|
|
4574
|
+
\u{1F4A1} Tip: You can type while the agent is working. Press Enter to queue
|
|
4575
|
+
your message. Press Esc to interrupt and send immediately.`
|
|
4318
4576
|
}]);
|
|
4319
4577
|
return true;
|
|
4320
4578
|
case "clear":
|
|
@@ -4564,8 +4822,169 @@ Last API Call Cost:
|
|
|
4564
4822
|
\u{1F4A1} Tip: Use /model haiku for cheaper operations, /compact to reduce context.`
|
|
4565
4823
|
}]);
|
|
4566
4824
|
return true;
|
|
4825
|
+
case "save":
|
|
4826
|
+
try {
|
|
4827
|
+
const conversationHistory = agent.getConversationHistory();
|
|
4828
|
+
if (conversationHistory.length === 0) {
|
|
4829
|
+
setMessages((prev) => [...prev, {
|
|
4830
|
+
role: "system",
|
|
4831
|
+
content: "\u274C Nothing to save - conversation is empty."
|
|
4832
|
+
}]);
|
|
4833
|
+
return true;
|
|
4834
|
+
}
|
|
4835
|
+
const savedSession = sessionManager2.saveSession(
|
|
4836
|
+
conversationHistory,
|
|
4837
|
+
agent.getModel(),
|
|
4838
|
+
agent.getProvider(),
|
|
4839
|
+
args || void 0,
|
|
4840
|
+
currentSessionId || void 0
|
|
4841
|
+
);
|
|
4842
|
+
setCurrentSessionId(savedSession.id);
|
|
4843
|
+
setMessages((prev) => [...prev, {
|
|
4844
|
+
role: "system",
|
|
4845
|
+
content: `\u2705 Session saved: "${savedSession.name}"
|
|
4846
|
+
ID: ${savedSession.id}
|
|
4847
|
+
Messages: ${conversationHistory.length}`
|
|
4848
|
+
}]);
|
|
4849
|
+
} catch (err) {
|
|
4850
|
+
setMessages((prev) => [...prev, {
|
|
4851
|
+
role: "system",
|
|
4852
|
+
content: `\u274C Failed to save session: ${err instanceof Error ? err.message : String(err)}`
|
|
4853
|
+
}]);
|
|
4854
|
+
}
|
|
4855
|
+
return true;
|
|
4856
|
+
case "resume":
|
|
4857
|
+
try {
|
|
4858
|
+
const lastSession = sessionManager2.getLastSession();
|
|
4859
|
+
if (!lastSession) {
|
|
4860
|
+
setMessages((prev) => [...prev, {
|
|
4861
|
+
role: "system",
|
|
4862
|
+
content: "\u274C No previous session to resume."
|
|
4863
|
+
}]);
|
|
4864
|
+
return true;
|
|
4865
|
+
}
|
|
4866
|
+
agent.setConversationHistory(lastSession.messages);
|
|
4867
|
+
agent.setModel(lastSession.model);
|
|
4868
|
+
if (lastSession.provider) {
|
|
4869
|
+
agent.setProvider(lastSession.provider);
|
|
4870
|
+
}
|
|
4871
|
+
setCurrentSessionId(lastSession.id);
|
|
4872
|
+
setMessages([{
|
|
4873
|
+
role: "system",
|
|
4874
|
+
content: `\u2705 Resumed session: "${lastSession.name}"
|
|
4875
|
+
${lastSession.messages.length} messages loaded
|
|
4876
|
+
Model: ${lastSession.model} (${lastSession.provider})`
|
|
4877
|
+
}]);
|
|
4878
|
+
} catch (err) {
|
|
4879
|
+
setMessages((prev) => [...prev, {
|
|
4880
|
+
role: "system",
|
|
4881
|
+
content: `\u274C Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
|
|
4882
|
+
}]);
|
|
4883
|
+
}
|
|
4884
|
+
return true;
|
|
4885
|
+
case "sessions":
|
|
4886
|
+
try {
|
|
4887
|
+
const sessions = sessionManager2.listSessions();
|
|
4888
|
+
if (sessions.length === 0) {
|
|
4889
|
+
setMessages((prev) => [...prev, {
|
|
4890
|
+
role: "system",
|
|
4891
|
+
content: "\u{1F4CB} No saved sessions."
|
|
4892
|
+
}]);
|
|
4893
|
+
return true;
|
|
4894
|
+
}
|
|
4895
|
+
const sessionList = sessions.slice(0, 10).map((s, i) => {
|
|
4896
|
+
const isCurrent = s.id === currentSessionId ? " (current)" : "";
|
|
4897
|
+
const date = new Date(s.updatedAt).toLocaleDateString();
|
|
4898
|
+
return ` ${i + 1}. ${s.name}${isCurrent}
|
|
4899
|
+
ID: ${s.id} | ${s.messageCount} msgs | ${date}`;
|
|
4900
|
+
}).join("\n\n");
|
|
4901
|
+
const moreText = sessions.length > 10 ? `
|
|
4902
|
+
|
|
4903
|
+
... and ${sessions.length - 10} more` : "";
|
|
4904
|
+
setMessages((prev) => [...prev, {
|
|
4905
|
+
role: "system",
|
|
4906
|
+
content: `\u{1F5C2}\uFE0F Saved Sessions:
|
|
4907
|
+
|
|
4908
|
+
${sessionList}${moreText}
|
|
4909
|
+
|
|
4910
|
+
Use /load <id> to load a session.`
|
|
4911
|
+
}]);
|
|
4912
|
+
} catch (err) {
|
|
4913
|
+
setMessages((prev) => [...prev, {
|
|
4914
|
+
role: "system",
|
|
4915
|
+
content: `\u274C Failed to list sessions: ${err instanceof Error ? err.message : String(err)}`
|
|
4916
|
+
}]);
|
|
4917
|
+
}
|
|
4918
|
+
return true;
|
|
4919
|
+
case "load":
|
|
4920
|
+
if (!args) {
|
|
4921
|
+
setMessages((prev) => [...prev, {
|
|
4922
|
+
role: "system",
|
|
4923
|
+
content: "\u2753 Usage: /load <session_id>\n Use /sessions to see saved sessions."
|
|
4924
|
+
}]);
|
|
4925
|
+
return true;
|
|
4926
|
+
}
|
|
4927
|
+
try {
|
|
4928
|
+
let loadSession = sessionManager2.getSession(args);
|
|
4929
|
+
if (!loadSession) {
|
|
4930
|
+
loadSession = sessionManager2.getSessionByName(args);
|
|
4931
|
+
}
|
|
4932
|
+
if (!loadSession) {
|
|
4933
|
+
setMessages((prev) => [...prev, {
|
|
4934
|
+
role: "system",
|
|
4935
|
+
content: `\u274C Session not found: "${args}"`
|
|
4936
|
+
}]);
|
|
4937
|
+
return true;
|
|
4938
|
+
}
|
|
4939
|
+
agent.setConversationHistory(loadSession.messages);
|
|
4940
|
+
agent.setModel(loadSession.model);
|
|
4941
|
+
if (loadSession.provider) {
|
|
4942
|
+
agent.setProvider(loadSession.provider);
|
|
4943
|
+
}
|
|
4944
|
+
setCurrentSessionId(loadSession.id);
|
|
4945
|
+
setMessages([{
|
|
4946
|
+
role: "system",
|
|
4947
|
+
content: `\u2705 Loaded session: "${loadSession.name}"
|
|
4948
|
+
${loadSession.messages.length} messages loaded
|
|
4949
|
+
Model: ${loadSession.model} (${loadSession.provider})`
|
|
4950
|
+
}]);
|
|
4951
|
+
} catch (err) {
|
|
4952
|
+
setMessages((prev) => [...prev, {
|
|
4953
|
+
role: "system",
|
|
4954
|
+
content: `\u274C Failed to load session: ${err instanceof Error ? err.message : String(err)}`
|
|
4955
|
+
}]);
|
|
4956
|
+
}
|
|
4957
|
+
return true;
|
|
4958
|
+
case "forget":
|
|
4959
|
+
try {
|
|
4960
|
+
sessionManager2.clearAllSessions();
|
|
4961
|
+
setCurrentSessionId(null);
|
|
4962
|
+
setMessages((prev) => [...prev, {
|
|
4963
|
+
role: "system",
|
|
4964
|
+
content: "\u2705 All sessions deleted."
|
|
4965
|
+
}]);
|
|
4966
|
+
} catch (err) {
|
|
4967
|
+
setMessages((prev) => [...prev, {
|
|
4968
|
+
role: "system",
|
|
4969
|
+
content: `\u274C Failed to clear sessions: ${err instanceof Error ? err.message : String(err)}`
|
|
4970
|
+
}]);
|
|
4971
|
+
}
|
|
4972
|
+
return true;
|
|
4567
4973
|
case "exit":
|
|
4568
4974
|
case "quit":
|
|
4975
|
+
try {
|
|
4976
|
+
const history = agent.getConversationHistory();
|
|
4977
|
+
if (history.length > 0) {
|
|
4978
|
+
sessionManager2.saveSession(
|
|
4979
|
+
history,
|
|
4980
|
+
agent.getModel(),
|
|
4981
|
+
agent.getProvider(),
|
|
4982
|
+
void 0,
|
|
4983
|
+
currentSessionId || void 0
|
|
4984
|
+
);
|
|
4985
|
+
}
|
|
4986
|
+
} catch {
|
|
4987
|
+
}
|
|
4569
4988
|
if (processManager.hasRunning()) {
|
|
4570
4989
|
processManager.killAll();
|
|
4571
4990
|
}
|
|
@@ -4582,7 +5001,18 @@ Last API Call Cost:
|
|
|
4582
5001
|
}, [agent, onExit, exit]);
|
|
4583
5002
|
const handleSubmit = useCallback(async (value) => {
|
|
4584
5003
|
const trimmed = value.trim();
|
|
4585
|
-
if (!trimmed
|
|
5004
|
+
if (!trimmed) return;
|
|
5005
|
+
if (isProcessing) {
|
|
5006
|
+
setQueuedInput(trimmed);
|
|
5007
|
+
setHasQueuedMessage(true);
|
|
5008
|
+
setInput("");
|
|
5009
|
+
setMessages((prev) => [...prev, {
|
|
5010
|
+
role: "system",
|
|
5011
|
+
content: `\u{1F4E5} Queued: "${trimmed.length > 50 ? trimmed.slice(0, 50) + "..." : trimmed}"
|
|
5012
|
+
Press Esc to interrupt and send now.`
|
|
5013
|
+
}]);
|
|
5014
|
+
return;
|
|
5015
|
+
}
|
|
4586
5016
|
if (trimmed.startsWith("/")) {
|
|
4587
5017
|
setInput("");
|
|
4588
5018
|
handleSlashCommand(trimmed);
|
|
@@ -4621,9 +5051,10 @@ Last API Call Cost:
|
|
|
4621
5051
|
} else {
|
|
4622
5052
|
setMessages((prev) => {
|
|
4623
5053
|
const filtered = prev.filter((m) => !m.isStreaming);
|
|
5054
|
+
const cleanedText = cleanModelOutput(result.text || "");
|
|
4624
5055
|
return [...filtered, {
|
|
4625
5056
|
role: "assistant",
|
|
4626
|
-
content:
|
|
5057
|
+
content: cleanedText || "(completed)",
|
|
4627
5058
|
toolCalls: result.toolCalls.map((tc) => ({
|
|
4628
5059
|
name: tc.name,
|
|
4629
5060
|
args: tc.input,
|
|
@@ -4637,21 +5068,25 @@ Last API Call Cost:
|
|
|
4637
5068
|
setStreamingText("");
|
|
4638
5069
|
setCurrentToolCalls([]);
|
|
4639
5070
|
} catch (err) {
|
|
4640
|
-
const errorMsg = err.message
|
|
5071
|
+
const errorMsg = err instanceof Error ? err.message : typeof err === "string" ? err : "Unknown error occurred";
|
|
4641
5072
|
let displayError = errorMsg;
|
|
4642
|
-
|
|
5073
|
+
const msgLower = errorMsg.toLowerCase();
|
|
5074
|
+
if (msgLower.includes("aborted") || msgLower.includes("aborterror")) {
|
|
4643
5075
|
setMessages((prev) => [...prev, {
|
|
4644
5076
|
role: "system",
|
|
4645
5077
|
content: "\u26A1 Generation interrupted by user."
|
|
4646
5078
|
}]);
|
|
4647
|
-
} else if (
|
|
4648
|
-
displayError = "
|
|
5079
|
+
} else if (msgLower.includes("credits exhausted")) {
|
|
5080
|
+
displayError = "API credits exhausted. Please add credits to your provider.";
|
|
5081
|
+
setError(displayError);
|
|
5082
|
+
} else if (msgLower.includes("invalid_api_key") || msgLower.includes("401") || msgLower.includes("unauthorized")) {
|
|
5083
|
+
displayError = 'Invalid API key. Run "quantish init" to reconfigure.';
|
|
4649
5084
|
setError(displayError);
|
|
4650
|
-
} else if (
|
|
4651
|
-
displayError =
|
|
5085
|
+
} else if (msgLower.includes("rate_limit") || msgLower.includes("429")) {
|
|
5086
|
+
displayError = "Rate limited. Please wait a moment and try again.";
|
|
4652
5087
|
setError(displayError);
|
|
4653
|
-
} else if (
|
|
4654
|
-
displayError = "
|
|
5088
|
+
} else if (msgLower.includes("cannot read properties of undefined") || msgLower.includes("undefined")) {
|
|
5089
|
+
displayError = "Tool call parsing error. The model may have sent malformed output.";
|
|
4655
5090
|
setError(displayError);
|
|
4656
5091
|
} else {
|
|
4657
5092
|
setError(displayError);
|
|
@@ -4660,8 +5095,16 @@ Last API Call Cost:
|
|
|
4660
5095
|
setIsProcessing(false);
|
|
4661
5096
|
setThinkingText(null);
|
|
4662
5097
|
abortController.current = null;
|
|
5098
|
+
if (hasQueuedMessage && queuedInput) {
|
|
5099
|
+
const nextMessage = queuedInput;
|
|
5100
|
+
setQueuedInput("");
|
|
5101
|
+
setHasQueuedMessage(false);
|
|
5102
|
+
setTimeout(() => {
|
|
5103
|
+
handleSubmit(nextMessage);
|
|
5104
|
+
}, 100);
|
|
5105
|
+
}
|
|
4663
5106
|
}
|
|
4664
|
-
}, [agent, isProcessing, isInterrupted, exit, onExit, handleSlashCommand]);
|
|
5107
|
+
}, [agent, isProcessing, isInterrupted, exit, onExit, handleSlashCommand, hasQueuedMessage, queuedInput]);
|
|
4665
5108
|
useEffect(() => {
|
|
4666
5109
|
const originalConfig = agent.config;
|
|
4667
5110
|
agent.config = {
|
|
@@ -4669,7 +5112,10 @@ Last API Call Cost:
|
|
|
4669
5112
|
streaming: true,
|
|
4670
5113
|
onText: (text, isComplete) => {
|
|
4671
5114
|
if (!isComplete) {
|
|
4672
|
-
|
|
5115
|
+
const cleanText = text.replace(/<tool_call>/g, "").replace(/<\/tool_call>/g, "").replace(/<arg_key>/g, "").replace(/<\/arg_key>/g, "");
|
|
5116
|
+
if (cleanText) {
|
|
5117
|
+
setStreamingText((prev) => prev + cleanText);
|
|
5118
|
+
}
|
|
4673
5119
|
}
|
|
4674
5120
|
},
|
|
4675
5121
|
onThinking: (text) => {
|
|
@@ -4716,10 +5162,17 @@ Stopped ${count} background process${count > 1 ? "es" : ""}.`);
|
|
|
4716
5162
|
if (key.escape && isProcessing) {
|
|
4717
5163
|
setIsInterrupted(true);
|
|
4718
5164
|
abortController.current?.abort();
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
5165
|
+
if (hasQueuedMessage && queuedInput) {
|
|
5166
|
+
setMessages((prev) => [...prev, {
|
|
5167
|
+
role: "system",
|
|
5168
|
+
content: "\u26A1 Interrupting and sending queued message..."
|
|
5169
|
+
}]);
|
|
5170
|
+
} else {
|
|
5171
|
+
setMessages((prev) => [...prev, {
|
|
5172
|
+
role: "system",
|
|
5173
|
+
content: "\u26A1 Interrupting..."
|
|
5174
|
+
}]);
|
|
5175
|
+
}
|
|
4723
5176
|
}
|
|
4724
5177
|
});
|
|
4725
5178
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
@@ -4796,22 +5249,29 @@ Stopped ${count} background process${count > 1 ? "es" : ""}.`);
|
|
|
4796
5249
|
] })
|
|
4797
5250
|
] }, i))
|
|
4798
5251
|
] }),
|
|
5252
|
+
hasQueuedMessage && isProcessing && /* @__PURE__ */ jsxs(Box, { marginBottom: 1, paddingLeft: 2, children: [
|
|
5253
|
+
/* @__PURE__ */ jsxs(Text, { color: "blue", children: [
|
|
5254
|
+
"\u{1F4E5} Queued: ",
|
|
5255
|
+
queuedInput.length > 40 ? queuedInput.slice(0, 40) + "..." : queuedInput
|
|
5256
|
+
] }),
|
|
5257
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: " (Esc to send now)" })
|
|
5258
|
+
] }),
|
|
4799
5259
|
/* @__PURE__ */ jsx(
|
|
4800
5260
|
Box,
|
|
4801
5261
|
{
|
|
4802
5262
|
borderStyle: "round",
|
|
4803
|
-
borderColor: isProcessing ? "gray" : "yellow",
|
|
5263
|
+
borderColor: hasQueuedMessage ? "blue" : isProcessing ? "gray" : "yellow",
|
|
4804
5264
|
paddingX: 1,
|
|
4805
5265
|
marginTop: 1,
|
|
4806
5266
|
children: /* @__PURE__ */ jsxs(Box, { children: [
|
|
4807
|
-
/* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: "\u276F " }),
|
|
5267
|
+
/* @__PURE__ */ jsx(Text, { color: hasQueuedMessage ? "blue" : "yellow", bold: true, children: "\u276F " }),
|
|
4808
5268
|
/* @__PURE__ */ jsx(
|
|
4809
5269
|
TextInput,
|
|
4810
5270
|
{
|
|
4811
5271
|
value: input,
|
|
4812
5272
|
onChange: setInput,
|
|
4813
5273
|
onSubmit: handleSubmit,
|
|
4814
|
-
placeholder: isProcessing ? "
|
|
5274
|
+
placeholder: hasQueuedMessage ? "Message queued. Type more or press Esc to send now." : isProcessing ? "Type to queue a message..." : "Ask anything or type / for commands"
|
|
4815
5275
|
}
|
|
4816
5276
|
)
|
|
4817
5277
|
] })
|