@elyracode/coding-agent 0.5.6 → 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";
@@ -54,6 +54,7 @@ import { SessionSelectorComponent } from "./components/session-selector.js";
54
54
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
55
55
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
56
56
  import { StartupAnimation } from "./components/startup-animation.js";
57
+ import { ThemeSelectorComponent } from "./components/theme-selector.js";
57
58
  import { ToolExecutionComponent } from "./components/tool-execution.js";
58
59
  import { TreeSelectorComponent } from "./components/tree-selector.js";
59
60
  import { UserMessageComponent } from "./components/user-message.js";
@@ -310,6 +311,21 @@ export class InteractiveMode {
310
311
  }));
311
312
  };
312
313
  }
314
+ const themeCommand = slashCommands.find((command) => command.name === "theme");
315
+ if (themeCommand) {
316
+ themeCommand.getArgumentCompletions = (prefix) => {
317
+ const themes = getAvailableThemes();
318
+ const currentTheme = this.settingsManager.getTheme() || "dark";
319
+ const filtered = themes.filter((t) => t.toLowerCase().includes(prefix.toLowerCase()));
320
+ if (filtered.length === 0)
321
+ return null;
322
+ return filtered.map((t) => ({
323
+ value: t,
324
+ label: t,
325
+ description: t === currentTheme ? "(current)" : undefined,
326
+ }));
327
+ };
328
+ }
313
329
  // Convert prompt templates to SlashCommand format for autocomplete
314
330
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
315
331
  name: cmd.name,
@@ -1930,6 +1946,42 @@ export class InteractiveMode {
1930
1946
  this.editor.setText("");
1931
1947
  return;
1932
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
+ }
1979
+ if (text === "/theme" || text.startsWith("/theme ")) {
1980
+ const themeName = text.startsWith("/theme ") ? text.slice(7).trim() : undefined;
1981
+ this.editor.setText("");
1982
+ this.handleThemeCommand(themeName);
1983
+ return;
1984
+ }
1933
1985
  if (text === "/scoped-models") {
1934
1986
  this.editor.setText("");
1935
1987
  await this.showModelsSelector();
@@ -3237,6 +3289,222 @@ export class InteractiveMode {
3237
3289
  return { component: selector, focus: selector.getSettingsList() };
3238
3290
  });
3239
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
+ }
3460
+ handleThemeCommand(themeName) {
3461
+ if (!themeName) {
3462
+ this.showThemeSelector();
3463
+ return;
3464
+ }
3465
+ const available = getAvailableThemes();
3466
+ if (!available.includes(themeName)) {
3467
+ this.showError(`Unknown theme: "${themeName}". Available: ${available.join(", ")}`);
3468
+ return;
3469
+ }
3470
+ const result = setTheme(themeName, true);
3471
+ this.settingsManager.setTheme(themeName);
3472
+ this.ui.invalidate();
3473
+ if (result.success) {
3474
+ this.showStatus(`Theme: ${themeName}`);
3475
+ }
3476
+ else {
3477
+ this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`);
3478
+ }
3479
+ }
3480
+ showThemeSelector() {
3481
+ const currentTheme = this.settingsManager.getTheme() || "dark";
3482
+ const previousTheme = currentTheme;
3483
+ this.showSelector((done) => {
3484
+ const selector = new ThemeSelectorComponent(currentTheme, (themeName) => {
3485
+ const result = setTheme(themeName, true);
3486
+ this.settingsManager.setTheme(themeName);
3487
+ this.ui.invalidate();
3488
+ done();
3489
+ if (!result.success) {
3490
+ this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`);
3491
+ }
3492
+ }, () => {
3493
+ // Cancel: restore previous theme
3494
+ setTheme(previousTheme, true);
3495
+ this.ui.invalidate();
3496
+ done();
3497
+ }, (themeName) => {
3498
+ // Preview on hover
3499
+ const result = setTheme(themeName, true);
3500
+ if (result.success) {
3501
+ this.ui.invalidate();
3502
+ this.ui.requestRender();
3503
+ }
3504
+ });
3505
+ return { component: selector, focus: selector.getSelectList() };
3506
+ });
3507
+ }
3240
3508
  async handleModelCommand(searchTerm) {
3241
3509
  if (!searchTerm) {
3242
3510
  this.showModelSelector();