@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.
- package/CHANGELOG.md +21 -0
- package/dist/core/agent-session.d.ts +7 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +21 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/session-manager.d.ts +11 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +20 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +5 -2
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/components/diff-viewer.d.ts +32 -0
- package/dist/modes/interactive/components/diff-viewer.d.ts.map +1 -0
- package/dist/modes/interactive/components/diff-viewer.js +136 -0
- package/dist/modes/interactive/components/diff-viewer.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +6 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +230 -71
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- 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",
|
|
3034
|
-
const updateInstruction = theme.fg("muted", "
|
|
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
|
-
|
|
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
|
-
|
|
3531
|
-
|
|
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
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
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
|
-
//
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
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
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
await
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
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
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
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
|
|
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
|
-
|
|
3806
|
-
const
|
|
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
|
|
3928
|
+
this.showStatus(pathArgs.length > 0 ? `No changes in ${pathArgs.join(", ")}` : "No changes");
|
|
3809
3929
|
return;
|
|
3810
3930
|
}
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
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.
|
|
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) {
|