@elyracode/coding-agent 0.9.0 → 0.9.1

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 (29) hide show
  1. package/CHANGELOG.md +13 -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 +18 -0
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/settings-manager.d.ts +3 -0
  7. package/dist/core/settings-manager.d.ts.map +1 -1
  8. package/dist/core/settings-manager.js +8 -0
  9. package/dist/core/settings-manager.js.map +1 -1
  10. package/dist/core/slash-commands.d.ts.map +1 -1
  11. package/dist/core/slash-commands.js +4 -1
  12. package/dist/core/slash-commands.js.map +1 -1
  13. package/dist/modes/interactive/components/diff-viewer.d.ts +32 -0
  14. package/dist/modes/interactive/components/diff-viewer.d.ts.map +1 -0
  15. package/dist/modes/interactive/components/diff-viewer.js +136 -0
  16. package/dist/modes/interactive/components/diff-viewer.js.map +1 -0
  17. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  18. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  19. package/dist/modes/interactive/components/settings-selector.js +10 -0
  20. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  21. package/dist/modes/interactive/interactive-mode.d.ts +6 -0
  22. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  23. package/dist/modes/interactive/interactive-mode.js +131 -15
  24. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  25. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  26. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  27. package/examples/extensions/sandbox/package.json +1 -1
  28. package/examples/extensions/with-deps/package.json +1 -1
  29. 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 ")) {
@@ -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
  },
@@ -3765,7 +3770,7 @@ export class InteractiveMode {
3765
3770
  ` ${theme.fg("muted", "/model")} Switch model`,
3766
3771
  ` ${theme.fg("muted", "/theme")} Switch theme`,
3767
3772
  ` ${theme.fg("muted", "/cost")} Token usage and cost`,
3768
- ` ${theme.fg("muted", "/diff")} Show uncommitted changes`,
3773
+ ` ${theme.fg("muted", "/diff")} Show changes (--session, --cached, or <path>)`,
3769
3774
  ` ${theme.fg("muted", "/pin <path>")} Pin file to context`,
3770
3775
  ` ${theme.fg("muted", "/memory")} View project memory`,
3771
3776
  ` ${theme.fg("muted", "/blueprint")} Apply session template`,
@@ -3799,25 +3804,136 @@ export class InteractiveMode {
3799
3804
  this.chatContainer.addChild(new Text(info, 1, 0));
3800
3805
  this.ui.requestRender();
3801
3806
  }
3802
- async handleDiffCommand() {
3807
+ async handleDiffCommand(args) {
3803
3808
  const cwd = this.sessionManager.getCwd();
3809
+ // Parse flags and an optional path filter from the argument string.
3810
+ const tokens = args.split(/\s+/).filter(Boolean);
3811
+ let sessionMode = false;
3812
+ let cachedMode = false;
3813
+ let editorMode = false;
3814
+ let inlineMode = false;
3815
+ const pathArgs = [];
3816
+ for (const token of tokens) {
3817
+ if (token === "--session" || token === "-s")
3818
+ sessionMode = true;
3819
+ else if (token === "--cached" || token === "--staged")
3820
+ cachedMode = true;
3821
+ else if (token === "--editor" || token === "-e")
3822
+ editorMode = true;
3823
+ else if (token === "--inline")
3824
+ inlineMode = true;
3825
+ else
3826
+ pathArgs.push(token);
3827
+ }
3828
+ // Respect the diffInEditor preference unless an explicit flag overrides it.
3829
+ if (!editorMode && !inlineMode && this.settingsManager.getDiffInEditor()) {
3830
+ editorMode = true;
3831
+ }
3832
+ const runGit = (gitArgs) => {
3833
+ const result = spawnSync("git", gitArgs, {
3834
+ cwd,
3835
+ encoding: "utf-8",
3836
+ maxBuffer: 32 * 1024 * 1024,
3837
+ stdio: ["ignore", "pipe", "pipe"],
3838
+ });
3839
+ if (result.status !== 0 && result.error)
3840
+ throw result.error;
3841
+ return result.stdout ?? "";
3842
+ };
3804
3843
  try {
3805
- const { execSync } = await import("node:child_process");
3806
- const diff = execSync("git diff", { cwd, encoding: "utf-8", maxBuffer: 1024 * 1024 });
3844
+ // Verify this is a git repository up front for a clear error.
3845
+ const insideRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
3846
+ cwd,
3847
+ encoding: "utf-8",
3848
+ stdio: ["ignore", "pipe", "pipe"],
3849
+ });
3850
+ if (insideRepo.status !== 0) {
3851
+ this.showError("Not a git repository.");
3852
+ return;
3853
+ }
3854
+ let diff = "";
3855
+ let title;
3856
+ const pathSpec = pathArgs.length > 0 ? ["--", ...pathArgs] : [];
3857
+ if (sessionMode) {
3858
+ // Diff the working tree against the session's first checkpoint, so the
3859
+ // user sees only what the agent changed during this session.
3860
+ const baseline = this.session.getSessionBaselineRef();
3861
+ if (!baseline) {
3862
+ this.showStatus("No session baseline yet (the agent hasn't taken a turn, or this isn't a git repo).");
3863
+ return;
3864
+ }
3865
+ diff = runGit(["diff", baseline, ...pathSpec]);
3866
+ title = "Changes This Session";
3867
+ }
3868
+ else if (cachedMode) {
3869
+ diff = runGit(["diff", "--cached", ...pathSpec]);
3870
+ title = "Staged Changes";
3871
+ }
3872
+ else {
3873
+ diff = runGit(["diff", ...pathSpec]);
3874
+ title = "Uncommitted Changes";
3875
+ // Include untracked files (not shown by plain `git diff`) when not
3876
+ // filtering by an explicit path.
3877
+ if (pathArgs.length === 0) {
3878
+ const untracked = runGit(["ls-files", "--others", "--exclude-standard"]).split("\n").filter(Boolean);
3879
+ if (untracked.length > 0) {
3880
+ diff += `\n${untracked.map((f) => `+ (untracked) ${f}`).join("\n")}`;
3881
+ }
3882
+ }
3883
+ }
3807
3884
  if (!diff.trim()) {
3808
- this.showStatus("No uncommitted changes");
3885
+ this.showStatus(pathArgs.length > 0 ? `No changes in ${pathArgs.join(", ")}` : "No changes");
3809
3886
  return;
3810
3887
  }
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();
3888
+ // Open the diff directly in the user's editor when requested or preferred.
3889
+ if (editorMode) {
3890
+ this.openTextInExternalEditor(diff, ".diff");
3891
+ return;
3892
+ }
3893
+ // Default: show the scrollable in-TUI diff viewer.
3894
+ const diffText = diff;
3895
+ this.showSelector((done) => {
3896
+ const viewer = new DiffViewerComponent(this.ui, diffText, title, {
3897
+ onOpenEditor: () => this.openTextInExternalEditor(diffText, ".diff"),
3898
+ onClose: done,
3899
+ });
3900
+ return { component: viewer, focus: viewer };
3901
+ });
3818
3902
  }
3819
3903
  catch {
3820
- this.showError("Failed to run git diff. Is this a git repository?");
3904
+ this.showError("Failed to run git diff.");
3905
+ }
3906
+ }
3907
+ /**
3908
+ * Open read-only text content in the user's external editor ($VISUAL/$EDITOR).
3909
+ * Suspends the TUI while the editor runs, then restores it. The `suffix`
3910
+ * controls the temp file extension so editors apply the right syntax highlighting.
3911
+ */
3912
+ openTextInExternalEditor(content, suffix) {
3913
+ const editorCmd = process.env.VISUAL || process.env.EDITOR;
3914
+ if (!editorCmd) {
3915
+ this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
3916
+ return;
3917
+ }
3918
+ const tmpFile = path.join(os.tmpdir(), `elyra-diff-${Date.now()}${suffix}`);
3919
+ try {
3920
+ fs.writeFileSync(tmpFile, content, "utf-8");
3921
+ this.ui.stop();
3922
+ const [editor, ...editorArgs] = editorCmd.split(" ");
3923
+ spawnSync(editor, [...editorArgs, tmpFile], {
3924
+ stdio: "inherit",
3925
+ shell: process.platform === "win32",
3926
+ });
3927
+ }
3928
+ finally {
3929
+ try {
3930
+ fs.unlinkSync(tmpFile);
3931
+ }
3932
+ catch {
3933
+ // Ignore cleanup errors
3934
+ }
3935
+ this.ui.start();
3936
+ this.ui.requestRender(true);
3821
3937
  }
3822
3938
  }
3823
3939
  handlePinCommand(filePath) {