@elyracode/coding-agent 0.5.7 → 0.5.8

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.
@@ -9,7 +9,7 @@ import * as path from "node:path";
9
9
  import { getProviders, } from "@elyracode/ai";
10
10
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@elyracode/tui";
11
11
  import { spawn, spawnSync } from "child_process";
12
- import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
12
+ import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getBlueprintsDir, getDebugLogPath, getDocsPath, getProjectBlueprintsDir, getShareViewerUrl, VERSION, } from "../../config.js";
13
13
  import { parseSkillBlock } from "../../core/agent-session.js";
14
14
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
15
15
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
@@ -1946,6 +1946,36 @@ export class InteractiveMode {
1946
1946
  this.editor.setText("");
1947
1947
  return;
1948
1948
  }
1949
+ if (text === "/cost") {
1950
+ this.editor.setText("");
1951
+ this.handleCostCommand();
1952
+ return;
1953
+ }
1954
+ if (text === "/diff") {
1955
+ this.editor.setText("");
1956
+ await this.handleDiffCommand();
1957
+ return;
1958
+ }
1959
+ if (text === "/pin" || text.startsWith("/pin ")) {
1960
+ this.editor.setText("");
1961
+ this.handlePinCommand(text.startsWith("/pin ") ? text.slice(5).trim() : undefined);
1962
+ return;
1963
+ }
1964
+ if (text === "/unpin" || text.startsWith("/unpin ")) {
1965
+ this.editor.setText("");
1966
+ this.handleUnpinCommand(text.startsWith("/unpin ") ? text.slice(7).trim() : undefined);
1967
+ return;
1968
+ }
1969
+ if (text === "/pins") {
1970
+ this.editor.setText("");
1971
+ this.handlePinsCommand();
1972
+ return;
1973
+ }
1974
+ if (text === "/blueprint" || text.startsWith("/blueprint ")) {
1975
+ this.editor.setText("");
1976
+ this.handleBlueprintCommand(text.startsWith("/blueprint ") ? text.slice(11).trim() : undefined);
1977
+ return;
1978
+ }
1949
1979
  if (text === "/theme" || text.startsWith("/theme ")) {
1950
1980
  const themeName = text.startsWith("/theme ") ? text.slice(7).trim() : undefined;
1951
1981
  this.editor.setText("");
@@ -3259,6 +3289,174 @@ export class InteractiveMode {
3259
3289
  return { component: selector, focus: selector.getSettingsList() };
3260
3290
  });
3261
3291
  }
3292
+ handleCostCommand() {
3293
+ const stats = this.session.getSessionStats();
3294
+ const fmt = (n) => n.toLocaleString();
3295
+ const cost = stats.cost > 0 ? `$${stats.cost.toFixed(4)}` : "$0.00";
3296
+ let info = `${theme.bold("Session Cost")}\n\n`;
3297
+ info += `${theme.fg("dim", "Input:")} ${fmt(stats.tokens.input)} tokens\n`;
3298
+ info += `${theme.fg("dim", "Output:")} ${fmt(stats.tokens.output)} tokens\n`;
3299
+ info += `${theme.fg("dim", "Cache read:")} ${fmt(stats.tokens.cacheRead)} tokens\n`;
3300
+ info += `${theme.fg("dim", "Cache write:")} ${fmt(stats.tokens.cacheWrite)} tokens\n`;
3301
+ info += `${theme.fg("dim", "Total:")} ${fmt(stats.tokens.total)} tokens\n\n`;
3302
+ info += `${theme.fg("dim", "Estimated cost:")} ${theme.fg("accent", cost)}\n`;
3303
+ if (stats.contextUsage) {
3304
+ const ctxTokens = stats.contextUsage.tokens ?? 0;
3305
+ const pct = stats.contextUsage.contextWindow > 0 ? Math.round((ctxTokens / stats.contextUsage.contextWindow) * 100) : 0;
3306
+ info += `${theme.fg("dim", "Context:")} ${fmt(ctxTokens)} / ${fmt(stats.contextUsage.contextWindow)} (${pct}%)`;
3307
+ }
3308
+ this.chatContainer.addChild(new Spacer(1));
3309
+ this.chatContainer.addChild(new Text(info, 1, 0));
3310
+ this.ui.requestRender();
3311
+ }
3312
+ async handleDiffCommand() {
3313
+ const cwd = this.sessionManager.getCwd();
3314
+ try {
3315
+ const { execSync } = await import("node:child_process");
3316
+ const diff = execSync("git diff", { cwd, encoding: "utf-8", maxBuffer: 1024 * 1024 });
3317
+ if (!diff.trim()) {
3318
+ this.showStatus("No uncommitted changes");
3319
+ return;
3320
+ }
3321
+ this.chatContainer.addChild(new Spacer(1));
3322
+ this.chatContainer.addChild(new DynamicBorder());
3323
+ this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Uncommitted Changes")), 1, 0));
3324
+ this.chatContainer.addChild(new Spacer(1));
3325
+ this.chatContainer.addChild(new Markdown(`\`\`\`diff\n${diff}\n\`\`\``, 1, 1, this.getMarkdownThemeWithSettings()));
3326
+ this.chatContainer.addChild(new DynamicBorder());
3327
+ this.ui.requestRender();
3328
+ }
3329
+ catch {
3330
+ this.showError("Failed to run git diff. Is this a git repository?");
3331
+ }
3332
+ }
3333
+ handlePinCommand(filePath) {
3334
+ if (!filePath) {
3335
+ this.showError("Usage: /pin <file-path>");
3336
+ return;
3337
+ }
3338
+ const cwd = this.sessionManager.getCwd();
3339
+ const resolved = path.resolve(cwd, filePath);
3340
+ if (!fs.existsSync(resolved)) {
3341
+ this.showError(`File not found: ${filePath}`);
3342
+ return;
3343
+ }
3344
+ const rel = path.relative(cwd, resolved);
3345
+ this.settingsManager.addPinnedFile(rel);
3346
+ this.showStatus(`Pinned: ${rel}`);
3347
+ }
3348
+ handleUnpinCommand(filePath) {
3349
+ if (!filePath) {
3350
+ this.showError("Usage: /unpin <file-path>");
3351
+ return;
3352
+ }
3353
+ const cwd = this.sessionManager.getCwd();
3354
+ const rel = path.relative(cwd, path.resolve(cwd, filePath));
3355
+ const pinned = this.settingsManager.getPinnedFiles();
3356
+ if (!pinned.includes(rel)) {
3357
+ this.showError(`Not pinned: ${rel}`);
3358
+ return;
3359
+ }
3360
+ this.settingsManager.removePinnedFile(rel);
3361
+ this.showStatus(`Unpinned: ${rel}`);
3362
+ }
3363
+ handlePinsCommand() {
3364
+ const pinned = this.settingsManager.getPinnedFiles();
3365
+ if (pinned.length === 0) {
3366
+ this.showStatus("No pinned files");
3367
+ return;
3368
+ }
3369
+ let info = `${theme.bold("Pinned Files")}\n\n`;
3370
+ for (const file of pinned) {
3371
+ info += ` ${theme.fg("accent", file)}\n`;
3372
+ }
3373
+ info += `\n${theme.fg("dim", `${pinned.length} file(s) pinned to context`)}`;
3374
+ this.chatContainer.addChild(new Spacer(1));
3375
+ this.chatContainer.addChild(new Text(info, 1, 0));
3376
+ this.ui.requestRender();
3377
+ }
3378
+ handleBlueprintCommand(name) {
3379
+ const cwd = this.sessionManager.getCwd();
3380
+ const dirs = [getProjectBlueprintsDir(cwd), getBlueprintsDir()];
3381
+ const blueprints = new Map();
3382
+ for (const dir of dirs) {
3383
+ if (!fs.existsSync(dir))
3384
+ continue;
3385
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
3386
+ if (!entry.isFile() || !entry.name.endsWith(".md"))
3387
+ continue;
3388
+ const bpName = entry.name.slice(0, -3);
3389
+ if (!blueprints.has(bpName)) {
3390
+ blueprints.set(bpName, path.resolve(dir, entry.name));
3391
+ }
3392
+ }
3393
+ }
3394
+ if (!name) {
3395
+ if (blueprints.size === 0) {
3396
+ this.showStatus("No blueprints found. Add .md files to .elyra/blueprints/ or ~/.elyra/agent/blueprints/");
3397
+ return;
3398
+ }
3399
+ let info = `${theme.bold("Available Blueprints")}\n\n`;
3400
+ for (const [bpName] of blueprints) {
3401
+ info += ` ${theme.fg("accent", bpName)}\n`;
3402
+ }
3403
+ info += `\n${theme.fg("dim", "Usage: /blueprint <name>")}`;
3404
+ this.chatContainer.addChild(new Spacer(1));
3405
+ this.chatContainer.addChild(new Text(info, 1, 0));
3406
+ this.ui.requestRender();
3407
+ return;
3408
+ }
3409
+ const bpPath = blueprints.get(name);
3410
+ if (!bpPath) {
3411
+ const available = Array.from(blueprints.keys()).join(", ");
3412
+ this.showError(`Blueprint not found: "${name}". Available: ${available || "none"}`);
3413
+ return;
3414
+ }
3415
+ try {
3416
+ const content = fs.readFileSync(bpPath, "utf-8");
3417
+ // Parse optional YAML frontmatter
3418
+ let body = content;
3419
+ const pins = [];
3420
+ if (content.startsWith("---\n")) {
3421
+ const endIdx = content.indexOf("\n---\n", 4);
3422
+ if (endIdx !== -1) {
3423
+ const frontmatter = content.slice(4, endIdx);
3424
+ body = content.slice(endIdx + 5);
3425
+ for (const line of frontmatter.split("\n")) {
3426
+ const pinMatch = line.match(/^pin:\s*(.+)$/);
3427
+ if (pinMatch) {
3428
+ pins.push(...pinMatch[1]
3429
+ .split(",")
3430
+ .map((s) => s.trim())
3431
+ .filter(Boolean));
3432
+ }
3433
+ const pinsMatch = line.match(/^pins:\s*\[(.+)\]$/);
3434
+ if (pinsMatch) {
3435
+ pins.push(...pinsMatch[1]
3436
+ .split(",")
3437
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
3438
+ .filter(Boolean));
3439
+ }
3440
+ }
3441
+ }
3442
+ }
3443
+ // Apply pins from frontmatter
3444
+ for (const pin of pins) {
3445
+ const resolved = path.resolve(cwd, pin);
3446
+ if (fs.existsSync(resolved)) {
3447
+ const rel = path.relative(cwd, resolved);
3448
+ this.settingsManager.addPinnedFile(rel);
3449
+ }
3450
+ }
3451
+ // Send blueprint content as user message to the agent
3452
+ this.session.prompt(body.trim());
3453
+ const pinMsg = pins.length > 0 ? ` (pinned ${pins.length} file(s))` : "";
3454
+ this.showStatus(`Blueprint: ${name}${pinMsg}`);
3455
+ }
3456
+ catch {
3457
+ this.showError(`Failed to read blueprint: ${bpPath}`);
3458
+ }
3459
+ }
3262
3460
  handleThemeCommand(themeName) {
3263
3461
  if (!themeName) {
3264
3462
  this.showThemeSelector();