@elyracode/coding-agent 0.9.0 → 0.9.2

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/core/agent-session.d.ts +7 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +21 -0
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/session-manager.d.ts +11 -0
  7. package/dist/core/session-manager.d.ts.map +1 -1
  8. package/dist/core/session-manager.js +20 -0
  9. package/dist/core/session-manager.js.map +1 -1
  10. package/dist/core/settings-manager.d.ts +3 -0
  11. package/dist/core/settings-manager.d.ts.map +1 -1
  12. package/dist/core/settings-manager.js +8 -0
  13. package/dist/core/settings-manager.js.map +1 -1
  14. package/dist/core/slash-commands.d.ts.map +1 -1
  15. package/dist/core/slash-commands.js +5 -2
  16. package/dist/core/slash-commands.js.map +1 -1
  17. package/dist/modes/interactive/components/diff-viewer.d.ts +32 -0
  18. package/dist/modes/interactive/components/diff-viewer.d.ts.map +1 -0
  19. package/dist/modes/interactive/components/diff-viewer.js +136 -0
  20. package/dist/modes/interactive/components/diff-viewer.js.map +1 -0
  21. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  22. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  23. package/dist/modes/interactive/components/settings-selector.js +10 -0
  24. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.d.ts +6 -0
  26. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  27. package/dist/modes/interactive/interactive-mode.js +230 -71
  28. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  29. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  30. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  31. package/examples/extensions/sandbox/package.json +1 -1
  32. package/examples/extensions/with-deps/package.json +1 -1
  33. package/package.json +4 -4
@@ -40,6 +40,7 @@ import { CountdownTimer } from "./components/countdown-timer.js";
40
40
  import { CustomEditor } from "./components/custom-editor.js";
41
41
  import { CustomMessageComponent } from "./components/custom-message.js";
42
42
  import { DaxnutsComponent } from "./components/daxnuts.js";
43
+ import { DiffViewerComponent } from "./components/diff-viewer.js";
43
44
  import { DynamicBorder } from "./components/dynamic-border.js";
44
45
  import { EarendilAnnouncementComponent } from "./components/earendil-announcement.js";
45
46
  import { ExtensionEditorComponent } from "./components/extension-editor.js";
@@ -2027,9 +2028,9 @@ export class InteractiveMode {
2027
2028
  this.handleCostCommand();
2028
2029
  return;
2029
2030
  }
2030
- if (text === "/diff") {
2031
+ if (text === "/diff" || text.startsWith("/diff ")) {
2031
2032
  this.editor.setText("");
2032
- await this.handleDiffCommand();
2033
+ await this.handleDiffCommand(text.slice("/diff".length).trim());
2033
2034
  return;
2034
2035
  }
2035
2036
  if (text === "/pin" || text.startsWith("/pin ")) {
@@ -3030,8 +3031,8 @@ export class InteractiveMode {
3030
3031
  this.ui.requestRender();
3031
3032
  }
3032
3033
  showPackageUpdateNotification(packages) {
3033
- const action = theme.fg("accent", `${APP_NAME} update`);
3034
- const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action;
3034
+ const action = theme.fg("accent", "/update");
3035
+ const updateInstruction = theme.fg("muted", "Extension updates are available. Type ") + action;
3035
3036
  const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n");
3036
3037
  this.chatContainer.addChild(new Spacer(1));
3037
3038
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
@@ -3259,6 +3260,7 @@ export class InteractiveMode {
3259
3260
  smartRouting: this.settingsManager.getSmartRouting(),
3260
3261
  codebaseMemory: this.settingsManager.getCodebaseMemory(),
3261
3262
  autoSkills: this.settingsManager.getAutoSkills(),
3263
+ diffInEditor: this.settingsManager.getDiffInEditor(),
3262
3264
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
3263
3265
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
3264
3266
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
@@ -3352,6 +3354,9 @@ export class InteractiveMode {
3352
3354
  onAutoSkillsChange: (enabled) => {
3353
3355
  this.settingsManager.setAutoSkills(enabled);
3354
3356
  },
3357
+ onDiffInEditorChange: (enabled) => {
3358
+ this.settingsManager.setDiffInEditor(enabled);
3359
+ },
3355
3360
  onQuietStartupChange: (enabled) => {
3356
3361
  this.settingsManager.setQuietStartup(enabled);
3357
3362
  },
@@ -3523,71 +3528,114 @@ export class InteractiveMode {
3523
3528
  }
3524
3529
  }
3525
3530
  async handleInAppUpdate() {
3526
- // If no update detected yet, check now
3531
+ if (process.env.ELYRA_OFFLINE) {
3532
+ this.showStatus("Updates are disabled in offline mode.");
3533
+ return;
3534
+ }
3535
+ this.chatContainer.addChild(new Spacer(1));
3536
+ const statusText = new Text(theme.fg("dim", "Checking for updates..."), 1, 0);
3537
+ this.chatContainer.addChild(statusText);
3538
+ this.ui.requestRender();
3539
+ // Check for an Elyra version update (populates this._availableUpdate).
3527
3540
  if (!this._availableUpdate) {
3528
3541
  await this.checkForVersionUpdate();
3529
3542
  }
3530
- if (!this._availableUpdate) {
3531
- this.showStatus(`Already on latest version (${this.version})`);
3543
+ const elyraUpdate = this._availableUpdate;
3544
+ // Check for installed-extension updates.
3545
+ const packageManager = new DefaultPackageManager({
3546
+ cwd: this.sessionManager.getCwd(),
3547
+ agentDir: getAgentDir(),
3548
+ settingsManager: this.settingsManager,
3549
+ });
3550
+ let extensionUpdates = [];
3551
+ try {
3552
+ extensionUpdates = await packageManager.checkForAvailableUpdates();
3553
+ }
3554
+ catch {
3555
+ // Best-effort: a failed extension check shouldn't block an Elyra update.
3556
+ }
3557
+ if (!elyraUpdate && extensionUpdates.length === 0) {
3558
+ statusText.setText(theme.fg("dim", `Already up to date (Elyra ${this.version}, all extensions current)`));
3559
+ this.ui.requestRender();
3532
3560
  return;
3533
3561
  }
3534
- const { latest, current } = this._availableUpdate;
3535
- // Get the self-update command for this installation
3536
- const settingsManager = this.settingsManager;
3537
- const npmCommand = settingsManager.getGlobalSettings().npmCommand;
3538
- const selfUpdateCommand = getSelfUpdateCommand(PACKAGE_NAME, npmCommand);
3539
- if (!selfUpdateCommand) {
3540
- this.showError(`Cannot self-update this installation. Run manually:\n ${theme.fg("accent", `npm install -g ${PACKAGE_NAME}`)}`);
3562
+ // If an Elyra update is available, make sure we can actually self-update.
3563
+ let selfUpdateCommand;
3564
+ if (elyraUpdate) {
3565
+ const npmCommand = this.settingsManager.getGlobalSettings().npmCommand;
3566
+ selfUpdateCommand = getSelfUpdateCommand(PACKAGE_NAME, npmCommand);
3567
+ if (!selfUpdateCommand) {
3568
+ statusText.setText(theme.fg("error", `Cannot self-update this installation. Update Elyra manually: npm install -g ${PACKAGE_NAME}`));
3569
+ this.ui.requestRender();
3570
+ // We can still update extensions below.
3571
+ }
3572
+ }
3573
+ // Nothing actionable: Elyra can't self-update and there are no extension updates.
3574
+ if (extensionUpdates.length === 0 && !(elyraUpdate && selfUpdateCommand)) {
3541
3575
  return;
3542
3576
  }
3543
- // Show progress
3544
- this.chatContainer.addChild(new Spacer(1));
3545
- const updateText = new Text(theme.fg("accent", `Updating ${current} \u2192 ${latest}...`), 1, 0);
3546
- this.chatContainer.addChild(updateText);
3577
+ // Announce what is being updated.
3578
+ const parts = [];
3579
+ if (elyraUpdate && selfUpdateCommand)
3580
+ parts.push(`Elyra ${elyraUpdate.current} \u2192 ${elyraUpdate.latest}`);
3581
+ if (extensionUpdates.length > 0) {
3582
+ parts.push(`${extensionUpdates.length} extension${extensionUpdates.length === 1 ? "" : "s"}`);
3583
+ }
3584
+ statusText.setText(theme.fg("accent", `Updating ${parts.join(" and ")}...`));
3547
3585
  this.ui.requestRender();
3548
- try {
3549
- // Run each step of the update command
3550
- for (const step of selfUpdateCommand.steps ?? [selfUpdateCommand]) {
3551
- await new Promise((resolve, reject) => {
3552
- const child = spawn(step.command, step.args, {
3553
- stdio: ["ignore", "pipe", "pipe"],
3554
- shell: process.platform === "win32",
3555
- });
3556
- let stderr = "";
3557
- child.stderr?.on("data", (data) => {
3558
- stderr += data.toString();
3559
- });
3560
- child.on("close", (code) => {
3561
- if (code === 0)
3562
- resolve();
3563
- else
3564
- reject(new Error(stderr.trim() || `Exit code ${code}`));
3565
- });
3566
- child.on("error", reject);
3567
- });
3586
+ // 1. Update extensions while Elyra is still running.
3587
+ if (extensionUpdates.length > 0) {
3588
+ try {
3589
+ await packageManager.update();
3590
+ }
3591
+ catch (error) {
3592
+ const msg = error instanceof Error ? error.message : String(error);
3593
+ this.showError(`Extension update failed: ${msg}`);
3568
3594
  }
3569
- // Success — show message and restart
3570
- updateText.setText(theme.fg("accent", `Updated to ${latest}. Restarting...`));
3571
- this.ui.requestRender();
3572
- // Brief pause so the user sees the message
3573
- await new Promise((r) => setTimeout(r, 500));
3574
- // Restart: stop TUI, then exec the same command
3575
- this.stop();
3576
- await this.runtimeHost.dispose();
3577
- // Re-exec elyra with the same arguments
3578
- const args = process.argv.slice(1);
3579
- const child = spawn(process.argv[0], args, {
3580
- stdio: "inherit",
3581
- shell: false,
3582
- });
3583
- child.on("close", (code) => {
3584
- process.exit(code ?? 0);
3585
- });
3586
3595
  }
3587
- catch (error) {
3588
- const msg = error instanceof Error ? error.message : String(error);
3589
- this.showError(`Update failed: ${msg}\n\nRun manually: ${selfUpdateCommand.display}`);
3596
+ // 2. Self-update Elyra if a newer version is available and updatable.
3597
+ if (elyraUpdate && selfUpdateCommand) {
3598
+ try {
3599
+ for (const step of selfUpdateCommand.steps ?? [selfUpdateCommand]) {
3600
+ await new Promise((resolve, reject) => {
3601
+ const child = spawn(step.command, step.args, {
3602
+ stdio: ["ignore", "pipe", "pipe"],
3603
+ shell: process.platform === "win32",
3604
+ });
3605
+ let stderr = "";
3606
+ child.stderr?.on("data", (data) => {
3607
+ stderr += data.toString();
3608
+ });
3609
+ child.on("close", (code) => {
3610
+ if (code === 0)
3611
+ resolve();
3612
+ else
3613
+ reject(new Error(stderr.trim() || `Exit code ${code}`));
3614
+ });
3615
+ child.on("error", reject);
3616
+ });
3617
+ }
3618
+ }
3619
+ catch (error) {
3620
+ const msg = error instanceof Error ? error.message : String(error);
3621
+ this.showError(`Elyra update failed: ${msg}\n\nRun manually: ${selfUpdateCommand.display}`);
3622
+ return;
3623
+ }
3590
3624
  }
3625
+ // 3. Restart so the new Elyra and/or updated extensions load fresh.
3626
+ statusText.setText(theme.fg("accent", "Updated. Restarting..."));
3627
+ this.ui.requestRender();
3628
+ await new Promise((r) => setTimeout(r, 500));
3629
+ this.stop();
3630
+ await this.runtimeHost.dispose();
3631
+ const args = process.argv.slice(1);
3632
+ const child = spawn(process.argv[0], args, {
3633
+ stdio: "inherit",
3634
+ shell: false,
3635
+ });
3636
+ child.on("close", (code) => {
3637
+ process.exit(code ?? 0);
3638
+ });
3591
3639
  }
3592
3640
  handleGoalCommand(args) {
3593
3641
  // No args = show or clear goal
@@ -3765,7 +3813,7 @@ export class InteractiveMode {
3765
3813
  ` ${theme.fg("muted", "/model")} Switch model`,
3766
3814
  ` ${theme.fg("muted", "/theme")} Switch theme`,
3767
3815
  ` ${theme.fg("muted", "/cost")} Token usage and cost`,
3768
- ` ${theme.fg("muted", "/diff")} Show uncommitted changes`,
3816
+ ` ${theme.fg("muted", "/diff")} Show changes (--session, --cached, or <path>)`,
3769
3817
  ` ${theme.fg("muted", "/pin <path>")} Pin file to context`,
3770
3818
  ` ${theme.fg("muted", "/memory")} View project memory`,
3771
3819
  ` ${theme.fg("muted", "/blueprint")} Apply session template`,
@@ -3799,25 +3847,136 @@ export class InteractiveMode {
3799
3847
  this.chatContainer.addChild(new Text(info, 1, 0));
3800
3848
  this.ui.requestRender();
3801
3849
  }
3802
- async handleDiffCommand() {
3850
+ async handleDiffCommand(args) {
3803
3851
  const cwd = this.sessionManager.getCwd();
3852
+ // Parse flags and an optional path filter from the argument string.
3853
+ const tokens = args.split(/\s+/).filter(Boolean);
3854
+ let sessionMode = false;
3855
+ let cachedMode = false;
3856
+ let editorMode = false;
3857
+ let inlineMode = false;
3858
+ const pathArgs = [];
3859
+ for (const token of tokens) {
3860
+ if (token === "--session" || token === "-s")
3861
+ sessionMode = true;
3862
+ else if (token === "--cached" || token === "--staged")
3863
+ cachedMode = true;
3864
+ else if (token === "--editor" || token === "-e")
3865
+ editorMode = true;
3866
+ else if (token === "--inline")
3867
+ inlineMode = true;
3868
+ else
3869
+ pathArgs.push(token);
3870
+ }
3871
+ // Respect the diffInEditor preference unless an explicit flag overrides it.
3872
+ if (!editorMode && !inlineMode && this.settingsManager.getDiffInEditor()) {
3873
+ editorMode = true;
3874
+ }
3875
+ const runGit = (gitArgs) => {
3876
+ const result = spawnSync("git", gitArgs, {
3877
+ cwd,
3878
+ encoding: "utf-8",
3879
+ maxBuffer: 32 * 1024 * 1024,
3880
+ stdio: ["ignore", "pipe", "pipe"],
3881
+ });
3882
+ if (result.status !== 0 && result.error)
3883
+ throw result.error;
3884
+ return result.stdout ?? "";
3885
+ };
3804
3886
  try {
3805
- const { execSync } = await import("node:child_process");
3806
- const diff = execSync("git diff", { cwd, encoding: "utf-8", maxBuffer: 1024 * 1024 });
3887
+ // Verify this is a git repository up front for a clear error.
3888
+ const insideRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
3889
+ cwd,
3890
+ encoding: "utf-8",
3891
+ stdio: ["ignore", "pipe", "pipe"],
3892
+ });
3893
+ if (insideRepo.status !== 0) {
3894
+ this.showError("Not a git repository.");
3895
+ return;
3896
+ }
3897
+ let diff = "";
3898
+ let title;
3899
+ const pathSpec = pathArgs.length > 0 ? ["--", ...pathArgs] : [];
3900
+ if (sessionMode) {
3901
+ // Diff the working tree against the session's first checkpoint, so the
3902
+ // user sees only what the agent changed during this session.
3903
+ const baseline = this.session.getSessionBaselineRef();
3904
+ if (!baseline) {
3905
+ this.showStatus("No session baseline yet (the agent hasn't taken a turn, or this isn't a git repo).");
3906
+ return;
3907
+ }
3908
+ diff = runGit(["diff", baseline, ...pathSpec]);
3909
+ title = "Changes This Session";
3910
+ }
3911
+ else if (cachedMode) {
3912
+ diff = runGit(["diff", "--cached", ...pathSpec]);
3913
+ title = "Staged Changes";
3914
+ }
3915
+ else {
3916
+ diff = runGit(["diff", ...pathSpec]);
3917
+ title = "Uncommitted Changes";
3918
+ // Include untracked files (not shown by plain `git diff`) when not
3919
+ // filtering by an explicit path.
3920
+ if (pathArgs.length === 0) {
3921
+ const untracked = runGit(["ls-files", "--others", "--exclude-standard"]).split("\n").filter(Boolean);
3922
+ if (untracked.length > 0) {
3923
+ diff += `\n${untracked.map((f) => `+ (untracked) ${f}`).join("\n")}`;
3924
+ }
3925
+ }
3926
+ }
3807
3927
  if (!diff.trim()) {
3808
- this.showStatus("No uncommitted changes");
3928
+ this.showStatus(pathArgs.length > 0 ? `No changes in ${pathArgs.join(", ")}` : "No changes");
3809
3929
  return;
3810
3930
  }
3811
- this.chatContainer.addChild(new Spacer(1));
3812
- this.chatContainer.addChild(new DynamicBorder());
3813
- this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Uncommitted Changes")), 1, 0));
3814
- this.chatContainer.addChild(new Spacer(1));
3815
- this.chatContainer.addChild(new Markdown(`\`\`\`diff\n${diff}\n\`\`\``, 1, 1, this.getMarkdownThemeWithSettings()));
3816
- this.chatContainer.addChild(new DynamicBorder());
3817
- this.ui.requestRender();
3931
+ // Open the diff directly in the user's editor when requested or preferred.
3932
+ if (editorMode) {
3933
+ this.openTextInExternalEditor(diff, ".diff");
3934
+ return;
3935
+ }
3936
+ // Default: show the scrollable in-TUI diff viewer.
3937
+ const diffText = diff;
3938
+ this.showSelector((done) => {
3939
+ const viewer = new DiffViewerComponent(this.ui, diffText, title, {
3940
+ onOpenEditor: () => this.openTextInExternalEditor(diffText, ".diff"),
3941
+ onClose: done,
3942
+ });
3943
+ return { component: viewer, focus: viewer };
3944
+ });
3818
3945
  }
3819
3946
  catch {
3820
- this.showError("Failed to run git diff. Is this a git repository?");
3947
+ this.showError("Failed to run git diff.");
3948
+ }
3949
+ }
3950
+ /**
3951
+ * Open read-only text content in the user's external editor ($VISUAL/$EDITOR).
3952
+ * Suspends the TUI while the editor runs, then restores it. The `suffix`
3953
+ * controls the temp file extension so editors apply the right syntax highlighting.
3954
+ */
3955
+ openTextInExternalEditor(content, suffix) {
3956
+ const editorCmd = process.env.VISUAL || process.env.EDITOR;
3957
+ if (!editorCmd) {
3958
+ this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
3959
+ return;
3960
+ }
3961
+ const tmpFile = path.join(os.tmpdir(), `elyra-diff-${Date.now()}${suffix}`);
3962
+ try {
3963
+ fs.writeFileSync(tmpFile, content, "utf-8");
3964
+ this.ui.stop();
3965
+ const [editor, ...editorArgs] = editorCmd.split(" ");
3966
+ spawnSync(editor, [...editorArgs, tmpFile], {
3967
+ stdio: "inherit",
3968
+ shell: process.platform === "win32",
3969
+ });
3970
+ }
3971
+ finally {
3972
+ try {
3973
+ fs.unlinkSync(tmpFile);
3974
+ }
3975
+ catch {
3976
+ // Ignore cleanup errors
3977
+ }
3978
+ this.ui.start();
3979
+ this.ui.requestRender(true);
3821
3980
  }
3822
3981
  }
3823
3982
  handlePinCommand(filePath) {