@elyracode/coding-agent 0.8.4 → 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 (36) hide show
  1. package/CHANGELOG.md +18 -1
  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 +31 -1
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/settings-manager.d.ts +6 -0
  7. package/dist/core/settings-manager.d.ts.map +1 -1
  8. package/dist/core/settings-manager.js +16 -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/core/system-prompt.d.ts.map +1 -1
  14. package/dist/core/system-prompt.js +3 -0
  15. package/dist/core/system-prompt.js.map +1 -1
  16. package/dist/core/tools/skill-write.d.ts +46 -0
  17. package/dist/core/tools/skill-write.d.ts.map +1 -0
  18. package/dist/core/tools/skill-write.js +232 -0
  19. package/dist/core/tools/skill-write.js.map +1 -0
  20. package/dist/modes/interactive/components/diff-viewer.d.ts +32 -0
  21. package/dist/modes/interactive/components/diff-viewer.d.ts.map +1 -0
  22. package/dist/modes/interactive/components/diff-viewer.js +136 -0
  23. package/dist/modes/interactive/components/diff-viewer.js.map +1 -0
  24. package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  25. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  26. package/dist/modes/interactive/components/settings-selector.js +20 -0
  27. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  28. package/dist/modes/interactive/interactive-mode.d.ts +6 -0
  29. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  30. package/dist/modes/interactive/interactive-mode.js +135 -15
  31. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  32. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  33. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  34. package/examples/extensions/sandbox/package.json +1 -1
  35. package/examples/extensions/with-deps/package.json +1 -1
  36. package/package.json +4 -4
@@ -0,0 +1,232 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Text } from "@elyracode/tui";
4
+ import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
5
+ import { Type } from "typebox";
6
+ import { wrapToolDefinition } from "./tool-definition-wrapper.js";
7
+ /** Max name length per Agent Skills spec */
8
+ const MAX_NAME_LENGTH = 64;
9
+ /** Max description length per Agent Skills spec */
10
+ const MAX_DESCRIPTION_LENGTH = 1024;
11
+ const skillWriteSchema = Type.Object({
12
+ name: Type.String({
13
+ description: "Skill name: lowercase letters, digits, and hyphens only (e.g. 'stripe-webhooks'). Becomes the directory name and must be unique. Max 64 chars.",
14
+ }),
15
+ description: Type.String({
16
+ description: "One-sentence description of when this skill applies. Used by the agent to decide when to load the skill. Max 1024 chars.",
17
+ }),
18
+ body: Type.String({
19
+ description: "The skill body in Markdown. Document the reusable solution: when to use it, the steps, gotchas, and any commands or code patterns. Write it so a future session can follow it without rediscovering the solution.",
20
+ }),
21
+ scope: Type.Optional(Type.Union([Type.Literal("user"), Type.Literal("project")], {
22
+ description: "'user' saves to your global skills (available everywhere). 'project' saves into the project (committed to git, shared with the team). Default: user.",
23
+ })),
24
+ });
25
+ const defaultSkillWriteOperations = {
26
+ writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
27
+ mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => { }),
28
+ exists: (path) => existsSync(path),
29
+ };
30
+ /**
31
+ * Validate a skill name against the Agent Skills spec.
32
+ * Returns an error message, or null if valid.
33
+ */
34
+ export function validateSkillName(name) {
35
+ if (!name)
36
+ return "name is required";
37
+ if (name.length > MAX_NAME_LENGTH) {
38
+ return `name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`;
39
+ }
40
+ if (!/^[a-z0-9-]+$/.test(name)) {
41
+ return "name must contain only lowercase letters, digits, and hyphens";
42
+ }
43
+ if (name.startsWith("-") || name.endsWith("-")) {
44
+ return "name must not start or end with a hyphen";
45
+ }
46
+ return null;
47
+ }
48
+ /** Validate a skill description against the spec. Returns an error message, or null if valid. */
49
+ export function validateSkillDescription(description) {
50
+ if (!description?.trim())
51
+ return "description is required";
52
+ if (description.length > MAX_DESCRIPTION_LENGTH) {
53
+ return `description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`;
54
+ }
55
+ return null;
56
+ }
57
+ /** Escape a YAML frontmatter string value. */
58
+ function yamlString(value) {
59
+ // Use double quotes and escape backslashes and quotes; collapse newlines.
60
+ const oneLine = value.replace(/\r?\n/g, " ").trim();
61
+ const escaped = oneLine.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
62
+ return `"${escaped}"`;
63
+ }
64
+ /** Build the full SKILL.md content from frontmatter fields plus a Markdown body. */
65
+ export function buildSkillMarkdown(name, description, body) {
66
+ const frontmatter = ["---", `name: ${name}`, `description: ${yamlString(description)}`, "---", ""].join("\n");
67
+ const trimmedBody = body.replace(/^\n+/, "").replace(/\s+$/, "");
68
+ return `${frontmatter}\n${trimmedBody}\n`;
69
+ }
70
+ /**
71
+ * Ask the user to approve saving a proposed skill.
72
+ * Returns true to save, false to reject. Falls back to true when no UI is present.
73
+ */
74
+ async function confirmSave(ctx, name, scope, description) {
75
+ if (!ctx?.hasUI)
76
+ return true;
77
+ const result = await ctx.ui.custom((tui, theme, _kb, done) => {
78
+ let index = 0; // 0 = Save, 1 = Reject
79
+ const options = ["Save skill", "Discard"];
80
+ let cachedLines;
81
+ function refresh() {
82
+ cachedLines = undefined;
83
+ tui.requestRender();
84
+ }
85
+ function handleInput(data) {
86
+ // Up/down arrows
87
+ if (data === "\x1b[A" || data === "\x1b[B") {
88
+ index = index === 0 ? 1 : 0;
89
+ refresh();
90
+ return;
91
+ }
92
+ // Enter
93
+ if (data === "\r" || data === "\n") {
94
+ done(index === 0);
95
+ return;
96
+ }
97
+ // Escape rejects
98
+ if (data === "\x1b") {
99
+ done(false);
100
+ }
101
+ }
102
+ function render(width) {
103
+ if (cachedLines)
104
+ return cachedLines;
105
+ const lines = [];
106
+ const rule = "\u2500".repeat(width);
107
+ lines.push(theme.fg("accent", rule));
108
+ lines.push(theme.fg("text", ` Elyra learned something. Save it as a reusable skill?`));
109
+ lines.push("");
110
+ lines.push(` ${theme.fg("accent", name)} ${theme.fg("muted", `(${scope})`)}`);
111
+ lines.push(` ${theme.fg("muted", description)}`);
112
+ lines.push("");
113
+ for (let i = 0; i < options.length; i++) {
114
+ const selected = i === index;
115
+ const prefix = selected ? theme.fg("accent", "> ") : " ";
116
+ const label = selected ? theme.fg("accent", options[i]) : theme.fg("text", options[i]);
117
+ lines.push(prefix + label);
118
+ }
119
+ lines.push("");
120
+ lines.push(theme.fg("dim", " \u2191\u2193 navigate \u2022 Enter to confirm \u2022 Esc to discard"));
121
+ lines.push(theme.fg("accent", rule));
122
+ cachedLines = lines;
123
+ return lines;
124
+ }
125
+ return {
126
+ render,
127
+ invalidate: () => {
128
+ cachedLines = undefined;
129
+ },
130
+ handleInput,
131
+ };
132
+ });
133
+ return result === true;
134
+ }
135
+ export function createSkillWriteToolDefinition(options) {
136
+ const ops = options.operations ?? defaultSkillWriteOperations;
137
+ return {
138
+ name: "skill_write",
139
+ label: "skill write",
140
+ description: "Save a hard-won solution as a reusable skill so future sessions never re-solve it. Use this AFTER you have solved a genuinely difficult or non-obvious problem that is likely to recur: a tricky setup, a project-specific workflow, a subtle integration, or a debugging insight. Do NOT use it for routine edits or one-off tasks. The skill becomes available in the next session.",
141
+ promptSnippet: "Save a reusable skill from a hard-won solution",
142
+ promptGuidelines: [
143
+ "Only write a skill when the solution was non-obvious and is likely to recur. Avoid skill spam.",
144
+ "Write the body so a future session can follow it without rediscovering the solution.",
145
+ "Prefer 'project' scope for project-specific workflows, 'user' scope for general techniques.",
146
+ ],
147
+ parameters: skillWriteSchema,
148
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
149
+ const { name, description, body } = params;
150
+ const scope = params.scope === "project" ? "project" : "user";
151
+ const baseDetails = { name, scope, path: null, saved: false };
152
+ const nameError = validateSkillName(name);
153
+ if (nameError)
154
+ throw new Error(`Invalid skill name: ${nameError}`);
155
+ const descError = validateSkillDescription(description);
156
+ if (descError)
157
+ throw new Error(`Invalid skill description: ${descError}`);
158
+ if (!body?.trim()) {
159
+ throw new Error("Skill body is required and must not be empty.");
160
+ }
161
+ const skillsRoot = scope === "project" ? options.projectSkillsDir : options.userSkillsDir;
162
+ const skillDir = join(skillsRoot, name);
163
+ const skillPath = join(skillDir, "SKILL.md");
164
+ if (ops.exists(skillPath)) {
165
+ throw new Error(`A skill named "${name}" already exists at ${skillPath}. Choose a different name or edit the existing file directly.`);
166
+ }
167
+ if (signal?.aborted) {
168
+ throw new Error("Operation aborted.");
169
+ }
170
+ // Approval: skip when auto-skills is on or no UI is available.
171
+ if (!options.getAutoSkills()) {
172
+ const approved = await confirmSave(ctx, name, scope, description);
173
+ if (!approved) {
174
+ return {
175
+ content: [{ type: "text", text: `Skill "${name}" was not saved (user declined).` }],
176
+ details: { ...baseDetails, rejected: true },
177
+ };
178
+ }
179
+ }
180
+ if (signal?.aborted) {
181
+ throw new Error("Operation aborted.");
182
+ }
183
+ const markdown = buildSkillMarkdown(name, description, body);
184
+ await ops.mkdir(skillDir);
185
+ await ops.writeFile(skillPath, markdown);
186
+ return {
187
+ content: [
188
+ {
189
+ type: "text",
190
+ text: `Saved skill "${name}" (${scope}) to ${skillPath}. It will be available in the next session.`,
191
+ },
192
+ ],
193
+ details: { ...baseDetails, path: skillPath, saved: true },
194
+ };
195
+ },
196
+ renderCall(args, theme, context) {
197
+ const renderArgs = args;
198
+ const name = typeof renderArgs?.name === "string" ? renderArgs.name : "...";
199
+ const scope = renderArgs?.scope === "project" ? "project" : "user";
200
+ const text = `${theme.fg("toolTitle", theme.bold("skill write"))} ${theme.fg("accent", name)} ` +
201
+ theme.fg("muted", `(${scope})`);
202
+ const component = context.lastComponent ?? new Text("", 0, 0);
203
+ component.setText(text);
204
+ return component;
205
+ },
206
+ renderResult(result, _options, theme, context) {
207
+ const details = result.details;
208
+ const text = context.lastComponent ?? new Text("", 0, 0);
209
+ if (details?.rejected) {
210
+ text.setText(theme.fg("warning", "Discarded"));
211
+ }
212
+ else if (details?.saved) {
213
+ text.setText(theme.fg("success", "\u2713 Saved \u2022 active next session"));
214
+ }
215
+ else if (context.isError) {
216
+ const output = result.content
217
+ .filter((c) => c.type === "text")
218
+ .map((c) => ("text" in c ? c.text : ""))
219
+ .join("\n");
220
+ text.setText(theme.fg("error", output));
221
+ }
222
+ else {
223
+ text.setText("");
224
+ }
225
+ return text;
226
+ },
227
+ };
228
+ }
229
+ export function createSkillWriteTool(options) {
230
+ return wrapToolDefinition(createSkillWriteToolDefinition(options));
231
+ }
232
+ //# sourceMappingURL=skill-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-write.js","sourceRoot":"","sources":["../../../src/core/tools/skill-write.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@elyracode/agent-core\";\nimport { Text } from \"@elyracode/tui\";\nimport { mkdir as fsMkdir, writeFile as fsWriteFile } from \"fs/promises\";\nimport { type Static, Type } from \"typebox\";\nimport type { ExtensionContext, ToolDefinition } from \"../extensions/types.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\n/** Max name length per Agent Skills spec */\nconst MAX_NAME_LENGTH = 64;\n/** Max description length per Agent Skills spec */\nconst MAX_DESCRIPTION_LENGTH = 1024;\n\nconst skillWriteSchema = Type.Object({\n\tname: Type.String({\n\t\tdescription:\n\t\t\t\"Skill name: lowercase letters, digits, and hyphens only (e.g. 'stripe-webhooks'). Becomes the directory name and must be unique. Max 64 chars.\",\n\t}),\n\tdescription: Type.String({\n\t\tdescription:\n\t\t\t\"One-sentence description of when this skill applies. Used by the agent to decide when to load the skill. Max 1024 chars.\",\n\t}),\n\tbody: Type.String({\n\t\tdescription:\n\t\t\t\"The skill body in Markdown. Document the reusable solution: when to use it, the steps, gotchas, and any commands or code patterns. Write it so a future session can follow it without rediscovering the solution.\",\n\t}),\n\tscope: Type.Optional(\n\t\tType.Union([Type.Literal(\"user\"), Type.Literal(\"project\")], {\n\t\t\tdescription:\n\t\t\t\t\"'user' saves to your global skills (available everywhere). 'project' saves into the project (committed to git, shared with the team). Default: user.\",\n\t\t}),\n\t),\n});\n\nexport type SkillWriteToolInput = Static<typeof skillWriteSchema>;\n\nexport interface SkillWriteToolDetails {\n\tname: string;\n\tscope: \"user\" | \"project\";\n\tpath: string | null;\n\tsaved: boolean;\n\trejected?: boolean;\n}\n\n/** Pluggable operations for the skill-write tool (overridable for tests). */\nexport interface SkillWriteOperations {\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\tmkdir: (dir: string) => Promise<void>;\n\texists: (path: string) => boolean;\n}\n\nconst defaultSkillWriteOperations: SkillWriteOperations = {\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\tmkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}),\n\texists: (path) => existsSync(path),\n};\n\nexport interface SkillWriteToolOptions {\n\t/** Directory for user-scoped skills (e.g. ~/.elyra/agent/skills). */\n\tuserSkillsDir: string;\n\t/** Directory for project-scoped skills (e.g. <cwd>/.elyra/skills). */\n\tprojectSkillsDir: string;\n\t/** Whether auto-skills is enabled. Read fresh on each call so toggles take effect immediately. */\n\tgetAutoSkills: () => boolean;\n\t/** Custom operations (default: local filesystem). */\n\toperations?: SkillWriteOperations;\n}\n\n/**\n * Validate a skill name against the Agent Skills spec.\n * Returns an error message, or null if valid.\n */\nexport function validateSkillName(name: string): string | null {\n\tif (!name) return \"name is required\";\n\tif (name.length > MAX_NAME_LENGTH) {\n\t\treturn `name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`;\n\t}\n\tif (!/^[a-z0-9-]+$/.test(name)) {\n\t\treturn \"name must contain only lowercase letters, digits, and hyphens\";\n\t}\n\tif (name.startsWith(\"-\") || name.endsWith(\"-\")) {\n\t\treturn \"name must not start or end with a hyphen\";\n\t}\n\treturn null;\n}\n\n/** Validate a skill description against the spec. Returns an error message, or null if valid. */\nexport function validateSkillDescription(description: string): string | null {\n\tif (!description?.trim()) return \"description is required\";\n\tif (description.length > MAX_DESCRIPTION_LENGTH) {\n\t\treturn `description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`;\n\t}\n\treturn null;\n}\n\n/** Escape a YAML frontmatter string value. */\nfunction yamlString(value: string): string {\n\t// Use double quotes and escape backslashes and quotes; collapse newlines.\n\tconst oneLine = value.replace(/\\r?\\n/g, \" \").trim();\n\tconst escaped = oneLine.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n\treturn `\"${escaped}\"`;\n}\n\n/** Build the full SKILL.md content from frontmatter fields plus a Markdown body. */\nexport function buildSkillMarkdown(name: string, description: string, body: string): string {\n\tconst frontmatter = [\"---\", `name: ${name}`, `description: ${yamlString(description)}`, \"---\", \"\"].join(\"\\n\");\n\tconst trimmedBody = body.replace(/^\\n+/, \"\").replace(/\\s+$/, \"\");\n\treturn `${frontmatter}\\n${trimmedBody}\\n`;\n}\n\n/**\n * Ask the user to approve saving a proposed skill.\n * Returns true to save, false to reject. Falls back to true when no UI is present.\n */\nasync function confirmSave(\n\tctx: ExtensionContext | undefined,\n\tname: string,\n\tscope: \"user\" | \"project\",\n\tdescription: string,\n): Promise<boolean> {\n\tif (!ctx?.hasUI) return true;\n\n\tconst result = await ctx.ui.custom<boolean>((tui, theme, _kb, done) => {\n\t\tlet index = 0; // 0 = Save, 1 = Reject\n\t\tconst options = [\"Save skill\", \"Discard\"];\n\t\tlet cachedLines: string[] | undefined;\n\n\t\tfunction refresh() {\n\t\t\tcachedLines = undefined;\n\t\t\ttui.requestRender();\n\t\t}\n\n\t\tfunction handleInput(data: string) {\n\t\t\t// Up/down arrows\n\t\t\tif (data === \"\\x1b[A\" || data === \"\\x1b[B\") {\n\t\t\t\tindex = index === 0 ? 1 : 0;\n\t\t\t\trefresh();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Enter\n\t\t\tif (data === \"\\r\" || data === \"\\n\") {\n\t\t\t\tdone(index === 0);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Escape rejects\n\t\t\tif (data === \"\\x1b\") {\n\t\t\t\tdone(false);\n\t\t\t}\n\t\t}\n\n\t\tfunction render(width: number): string[] {\n\t\t\tif (cachedLines) return cachedLines;\n\t\t\tconst lines: string[] = [];\n\t\t\tconst rule = \"\\u2500\".repeat(width);\n\t\t\tlines.push(theme.fg(\"accent\", rule));\n\t\t\tlines.push(theme.fg(\"text\", ` Elyra learned something. Save it as a reusable skill?`));\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(` ${theme.fg(\"accent\", name)} ${theme.fg(\"muted\", `(${scope})`)}`);\n\t\t\tlines.push(` ${theme.fg(\"muted\", description)}`);\n\t\t\tlines.push(\"\");\n\t\t\tfor (let i = 0; i < options.length; i++) {\n\t\t\t\tconst selected = i === index;\n\t\t\t\tconst prefix = selected ? theme.fg(\"accent\", \"> \") : \" \";\n\t\t\t\tconst label = selected ? theme.fg(\"accent\", options[i]) : theme.fg(\"text\", options[i]);\n\t\t\t\tlines.push(prefix + label);\n\t\t\t}\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(theme.fg(\"dim\", \" \\u2191\\u2193 navigate \\u2022 Enter to confirm \\u2022 Esc to discard\"));\n\t\t\tlines.push(theme.fg(\"accent\", rule));\n\t\t\tcachedLines = lines;\n\t\t\treturn lines;\n\t\t}\n\n\t\treturn {\n\t\t\trender,\n\t\t\tinvalidate: () => {\n\t\t\t\tcachedLines = undefined;\n\t\t\t},\n\t\t\thandleInput,\n\t\t};\n\t});\n\n\treturn result === true;\n}\n\nexport function createSkillWriteToolDefinition(\n\toptions: SkillWriteToolOptions,\n): ToolDefinition<typeof skillWriteSchema, SkillWriteToolDetails | undefined> {\n\tconst ops = options.operations ?? defaultSkillWriteOperations;\n\n\treturn {\n\t\tname: \"skill_write\",\n\t\tlabel: \"skill write\",\n\t\tdescription:\n\t\t\t\"Save a hard-won solution as a reusable skill so future sessions never re-solve it. Use this AFTER you have solved a genuinely difficult or non-obvious problem that is likely to recur: a tricky setup, a project-specific workflow, a subtle integration, or a debugging insight. Do NOT use it for routine edits or one-off tasks. The skill becomes available in the next session.\",\n\t\tpromptSnippet: \"Save a reusable skill from a hard-won solution\",\n\t\tpromptGuidelines: [\n\t\t\t\"Only write a skill when the solution was non-obvious and is likely to recur. Avoid skill spam.\",\n\t\t\t\"Write the body so a future session can follow it without rediscovering the solution.\",\n\t\t\t\"Prefer 'project' scope for project-specific workflows, 'user' scope for general techniques.\",\n\t\t],\n\t\tparameters: skillWriteSchema,\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx) {\n\t\t\tconst { name, description, body } = params;\n\t\t\tconst scope: \"user\" | \"project\" = params.scope === \"project\" ? \"project\" : \"user\";\n\t\t\tconst baseDetails: SkillWriteToolDetails = { name, scope, path: null, saved: false };\n\n\t\t\tconst nameError = validateSkillName(name);\n\t\t\tif (nameError) throw new Error(`Invalid skill name: ${nameError}`);\n\n\t\t\tconst descError = validateSkillDescription(description);\n\t\t\tif (descError) throw new Error(`Invalid skill description: ${descError}`);\n\n\t\t\tif (!body?.trim()) {\n\t\t\t\tthrow new Error(\"Skill body is required and must not be empty.\");\n\t\t\t}\n\n\t\t\tconst skillsRoot = scope === \"project\" ? options.projectSkillsDir : options.userSkillsDir;\n\t\t\tconst skillDir = join(skillsRoot, name);\n\t\t\tconst skillPath = join(skillDir, \"SKILL.md\");\n\n\t\t\tif (ops.exists(skillPath)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`A skill named \"${name}\" already exists at ${skillPath}. Choose a different name or edit the existing file directly.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Operation aborted.\");\n\t\t\t}\n\n\t\t\t// Approval: skip when auto-skills is on or no UI is available.\n\t\t\tif (!options.getAutoSkills()) {\n\t\t\t\tconst approved = await confirmSave(ctx, name, scope, description);\n\t\t\t\tif (!approved) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Skill \"${name}\" was not saved (user declined).` }],\n\t\t\t\t\t\tdetails: { ...baseDetails, rejected: true },\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Operation aborted.\");\n\t\t\t}\n\n\t\t\tconst markdown = buildSkillMarkdown(name, description, body);\n\t\t\tawait ops.mkdir(skillDir);\n\t\t\tawait ops.writeFile(skillPath, markdown);\n\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: `Saved skill \"${name}\" (${scope}) to ${skillPath}. It will be available in the next session.`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { ...baseDetails, path: skillPath, saved: true },\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst renderArgs = args as { name?: string; scope?: string } | undefined;\n\t\t\tconst name = typeof renderArgs?.name === \"string\" ? renderArgs.name : \"...\";\n\t\t\tconst scope = renderArgs?.scope === \"project\" ? \"project\" : \"user\";\n\t\t\tconst text =\n\t\t\t\t`${theme.fg(\"toolTitle\", theme.bold(\"skill write\"))} ${theme.fg(\"accent\", name)} ` +\n\t\t\t\ttheme.fg(\"muted\", `(${scope})`);\n\t\t\tconst component = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tcomponent.setText(text);\n\t\t\treturn component;\n\t\t},\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst details = result.details as SkillWriteToolDetails | undefined;\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tif (details?.rejected) {\n\t\t\t\ttext.setText(theme.fg(\"warning\", \"Discarded\"));\n\t\t\t} else if (details?.saved) {\n\t\t\t\ttext.setText(theme.fg(\"success\", \"\\u2713 Saved \\u2022 active next session\"));\n\t\t\t} else if (context.isError) {\n\t\t\t\tconst output = result.content\n\t\t\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t\t\t.map((c) => (\"text\" in c ? c.text : \"\"))\n\t\t\t\t\t.join(\"\\n\");\n\t\t\t\ttext.setText(theme.fg(\"error\", output));\n\t\t\t} else {\n\t\t\t\ttext.setText(\"\");\n\t\t\t}\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createSkillWriteTool(options: SkillWriteToolOptions): AgentTool<typeof skillWriteSchema> {\n\treturn wrapToolDefinition(createSkillWriteToolDefinition(options));\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import { type Component, type TUI } from "@elyracode/tui";
2
+ export interface DiffViewerCallbacks {
3
+ /** Open the diff in the external editor. */
4
+ onOpenEditor: () => void;
5
+ /** Close the viewer. */
6
+ onClose: () => void;
7
+ }
8
+ /**
9
+ * Full-height, scrollable diff viewer rendered in the editor area.
10
+ *
11
+ * Keys:
12
+ * - up/down: scroll one line
13
+ * - PageUp/PageDown: scroll one viewport
14
+ * - g / G (or Home / End): jump to top / bottom
15
+ * - e: open the diff in the external editor
16
+ * - Esc / q: close
17
+ */
18
+ export declare class DiffViewerComponent implements Component {
19
+ private tui;
20
+ private title;
21
+ private callbacks;
22
+ private readonly styledLines;
23
+ private scrollOffset;
24
+ private lastViewport;
25
+ constructor(tui: TUI, diffText: string, title: string, callbacks: DiffViewerCallbacks);
26
+ invalidate(): void;
27
+ private getViewportHeight;
28
+ private clampScroll;
29
+ render(width: number): string[];
30
+ handleInput(keyData: string): void;
31
+ }
32
+ //# sourceMappingURL=diff-viewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-viewer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/diff-viewer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAkB,KAAK,GAAG,EAAmB,MAAM,gBAAgB,CAAC;AAyC3F,MAAM,WAAW,mBAAmB;IACnC,4CAA4C;IAC5C,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,wBAAwB;IACxB,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,qBAAa,mBAAoB,YAAW,SAAS;IAMnD,OAAO,CAAC,GAAG;IAEX,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,SAAS;IARlB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAW;IACvC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,YAAY,CAAM;IAE1B,YACS,GAAG,EAAE,GAAG,EAChB,QAAQ,EAAE,MAAM,EACR,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,mBAAmB,EAGtC;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,WAAW;IAMnB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAsC9B;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA8BjC;CACD"}
@@ -0,0 +1,136 @@
1
+ import { getKeybindings, truncateToWidth } from "@elyracode/tui";
2
+ import { theme } from "../theme/theme.js";
3
+ /** Replace tabs with spaces for consistent column rendering. */
4
+ function replaceTabs(text) {
5
+ return text.replace(/\t/g, " ");
6
+ }
7
+ /**
8
+ * Colorize a raw unified-diff (git diff) line by its leading marker.
9
+ * Returns the styled line.
10
+ */
11
+ function colorizeGitDiffLine(line) {
12
+ const expanded = replaceTabs(line);
13
+ // Hunk header: @@ -a,b +c,d @@
14
+ if (expanded.startsWith("@@")) {
15
+ return theme.fg("accent", expanded);
16
+ }
17
+ // File headers and metadata.
18
+ if (expanded.startsWith("diff --git") ||
19
+ expanded.startsWith("index ") ||
20
+ expanded.startsWith("+++") ||
21
+ expanded.startsWith("---") ||
22
+ expanded.startsWith("new file") ||
23
+ expanded.startsWith("deleted file") ||
24
+ expanded.startsWith("rename ") ||
25
+ expanded.startsWith("similarity ") ||
26
+ expanded.startsWith("Binary files")) {
27
+ return theme.bold(theme.fg("muted", expanded));
28
+ }
29
+ if (expanded.startsWith("+")) {
30
+ return theme.fg("toolDiffAdded", expanded);
31
+ }
32
+ if (expanded.startsWith("-")) {
33
+ return theme.fg("toolDiffRemoved", expanded);
34
+ }
35
+ return theme.fg("toolDiffContext", expanded);
36
+ }
37
+ /**
38
+ * Full-height, scrollable diff viewer rendered in the editor area.
39
+ *
40
+ * Keys:
41
+ * - up/down: scroll one line
42
+ * - PageUp/PageDown: scroll one viewport
43
+ * - g / G (or Home / End): jump to top / bottom
44
+ * - e: open the diff in the external editor
45
+ * - Esc / q: close
46
+ */
47
+ export class DiffViewerComponent {
48
+ constructor(tui, diffText, title, callbacks) {
49
+ this.tui = tui;
50
+ this.title = title;
51
+ this.callbacks = callbacks;
52
+ this.scrollOffset = 0;
53
+ this.lastViewport = 10;
54
+ this.styledLines = diffText.replace(/\n$/, "").split("\n").map(colorizeGitDiffLine);
55
+ }
56
+ invalidate() {
57
+ // No cached layout state.
58
+ }
59
+ getViewportHeight() {
60
+ // Reserve rows for: title, top rule, bottom rule, hint line, and a small margin.
61
+ const reserved = 5;
62
+ const rows = this.tui.terminal.rows || 24;
63
+ return Math.max(5, rows - reserved);
64
+ }
65
+ clampScroll(viewport) {
66
+ const maxOffset = Math.max(0, this.styledLines.length - viewport);
67
+ if (this.scrollOffset < 0)
68
+ this.scrollOffset = 0;
69
+ if (this.scrollOffset > maxOffset)
70
+ this.scrollOffset = maxOffset;
71
+ }
72
+ render(width) {
73
+ const viewport = this.getViewportHeight();
74
+ this.lastViewport = viewport;
75
+ this.clampScroll(viewport);
76
+ const total = this.styledLines.length;
77
+ const start = this.scrollOffset;
78
+ const end = Math.min(start + viewport, total);
79
+ const lines = [];
80
+ // Title
81
+ lines.push(theme.bold(theme.fg("accent", truncateToWidth(this.title, width))));
82
+ // Top rule with position indicator
83
+ const above = start;
84
+ const topLabel = above > 0 ? `─── ↑ ${above} more ` : "";
85
+ lines.push(theme.fg("muted", truncateToWidth(topLabel + "─".repeat(Math.max(0, width)), width)));
86
+ // Visible content
87
+ for (let i = start; i < end; i++) {
88
+ lines.push(truncateToWidth(this.styledLines[i], width));
89
+ }
90
+ // Bottom rule with position indicator
91
+ const below = total - end;
92
+ const bottomLabel = below > 0 ? `─── ↓ ${below} more ` : "";
93
+ lines.push(theme.fg("muted", truncateToWidth(bottomLabel + "─".repeat(Math.max(0, width)), width)));
94
+ // Hint line
95
+ lines.push(theme.fg("dim", truncateToWidth(" ↑↓ scroll · PgUp/PgDn page · g/G top/bottom · e open in editor · Esc close", width)));
96
+ return lines;
97
+ }
98
+ handleInput(keyData) {
99
+ const kb = getKeybindings();
100
+ const viewport = this.lastViewport;
101
+ if (kb.matches(keyData, "tui.select.up")) {
102
+ this.scrollOffset -= 1;
103
+ }
104
+ else if (kb.matches(keyData, "tui.select.down")) {
105
+ this.scrollOffset += 1;
106
+ }
107
+ else if (kb.matches(keyData, "tui.select.pageUp")) {
108
+ this.scrollOffset -= viewport;
109
+ }
110
+ else if (kb.matches(keyData, "tui.select.pageDown")) {
111
+ this.scrollOffset += viewport;
112
+ }
113
+ else if (keyData === "g" || keyData === "\x1b[H") {
114
+ // g or Home → top
115
+ this.scrollOffset = 0;
116
+ }
117
+ else if (keyData === "G" || keyData === "\x1b[F") {
118
+ // G or End → bottom
119
+ this.scrollOffset = this.styledLines.length;
120
+ }
121
+ else if (keyData === "e") {
122
+ this.callbacks.onOpenEditor();
123
+ return;
124
+ }
125
+ else if (kb.matches(keyData, "tui.select.cancel") || keyData === "q") {
126
+ this.callbacks.onClose();
127
+ return;
128
+ }
129
+ else {
130
+ return;
131
+ }
132
+ this.clampScroll(viewport);
133
+ this.tui.requestRender();
134
+ }
135
+ }
136
+ //# sourceMappingURL=diff-viewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-viewer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/diff-viewer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, getKeybindings, type TUI, truncateToWidth } from \"@elyracode/tui\";\nimport { theme } from \"../theme/theme.js\";\n\n/** Replace tabs with spaces for consistent column rendering. */\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\n/**\n * Colorize a raw unified-diff (git diff) line by its leading marker.\n * Returns the styled line.\n */\nfunction colorizeGitDiffLine(line: string): string {\n\tconst expanded = replaceTabs(line);\n\t// Hunk header: @@ -a,b +c,d @@\n\tif (expanded.startsWith(\"@@\")) {\n\t\treturn theme.fg(\"accent\", expanded);\n\t}\n\t// File headers and metadata.\n\tif (\n\t\texpanded.startsWith(\"diff --git\") ||\n\t\texpanded.startsWith(\"index \") ||\n\t\texpanded.startsWith(\"+++\") ||\n\t\texpanded.startsWith(\"---\") ||\n\t\texpanded.startsWith(\"new file\") ||\n\t\texpanded.startsWith(\"deleted file\") ||\n\t\texpanded.startsWith(\"rename \") ||\n\t\texpanded.startsWith(\"similarity \") ||\n\t\texpanded.startsWith(\"Binary files\")\n\t) {\n\t\treturn theme.bold(theme.fg(\"muted\", expanded));\n\t}\n\tif (expanded.startsWith(\"+\")) {\n\t\treturn theme.fg(\"toolDiffAdded\", expanded);\n\t}\n\tif (expanded.startsWith(\"-\")) {\n\t\treturn theme.fg(\"toolDiffRemoved\", expanded);\n\t}\n\treturn theme.fg(\"toolDiffContext\", expanded);\n}\n\nexport interface DiffViewerCallbacks {\n\t/** Open the diff in the external editor. */\n\tonOpenEditor: () => void;\n\t/** Close the viewer. */\n\tonClose: () => void;\n}\n\n/**\n * Full-height, scrollable diff viewer rendered in the editor area.\n *\n * Keys:\n * - up/down: scroll one line\n * - PageUp/PageDown: scroll one viewport\n * - g / G (or Home / End): jump to top / bottom\n * - e: open the diff in the external editor\n * - Esc / q: close\n */\nexport class DiffViewerComponent implements Component {\n\tprivate readonly styledLines: string[];\n\tprivate scrollOffset = 0;\n\tprivate lastViewport = 10;\n\n\tconstructor(\n\t\tprivate tui: TUI,\n\t\tdiffText: string,\n\t\tprivate title: string,\n\t\tprivate callbacks: DiffViewerCallbacks,\n\t) {\n\t\tthis.styledLines = diffText.replace(/\\n$/, \"\").split(\"\\n\").map(colorizeGitDiffLine);\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached layout state.\n\t}\n\n\tprivate getViewportHeight(): number {\n\t\t// Reserve rows for: title, top rule, bottom rule, hint line, and a small margin.\n\t\tconst reserved = 5;\n\t\tconst rows = this.tui.terminal.rows || 24;\n\t\treturn Math.max(5, rows - reserved);\n\t}\n\n\tprivate clampScroll(viewport: number): void {\n\t\tconst maxOffset = Math.max(0, this.styledLines.length - viewport);\n\t\tif (this.scrollOffset < 0) this.scrollOffset = 0;\n\t\tif (this.scrollOffset > maxOffset) this.scrollOffset = maxOffset;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst viewport = this.getViewportHeight();\n\t\tthis.lastViewport = viewport;\n\t\tthis.clampScroll(viewport);\n\n\t\tconst total = this.styledLines.length;\n\t\tconst start = this.scrollOffset;\n\t\tconst end = Math.min(start + viewport, total);\n\n\t\tconst lines: string[] = [];\n\n\t\t// Title\n\t\tlines.push(theme.bold(theme.fg(\"accent\", truncateToWidth(this.title, width))));\n\n\t\t// Top rule with position indicator\n\t\tconst above = start;\n\t\tconst topLabel = above > 0 ? `─── ↑ ${above} more ` : \"\";\n\t\tlines.push(theme.fg(\"muted\", truncateToWidth(topLabel + \"─\".repeat(Math.max(0, width)), width)));\n\n\t\t// Visible content\n\t\tfor (let i = start; i < end; i++) {\n\t\t\tlines.push(truncateToWidth(this.styledLines[i], width));\n\t\t}\n\n\t\t// Bottom rule with position indicator\n\t\tconst below = total - end;\n\t\tconst bottomLabel = below > 0 ? `─── ↓ ${below} more ` : \"\";\n\t\tlines.push(theme.fg(\"muted\", truncateToWidth(bottomLabel + \"─\".repeat(Math.max(0, width)), width)));\n\n\t\t// Hint line\n\t\tlines.push(\n\t\t\ttheme.fg(\n\t\t\t\t\"dim\",\n\t\t\t\ttruncateToWidth(\" ↑↓ scroll · PgUp/PgDn page · g/G top/bottom · e open in editor · Esc close\", width),\n\t\t\t),\n\t\t);\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tconst viewport = this.lastViewport;\n\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tthis.scrollOffset -= 1;\n\t\t} else if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tthis.scrollOffset += 1;\n\t\t} else if (kb.matches(keyData, \"tui.select.pageUp\")) {\n\t\t\tthis.scrollOffset -= viewport;\n\t\t} else if (kb.matches(keyData, \"tui.select.pageDown\")) {\n\t\t\tthis.scrollOffset += viewport;\n\t\t} else if (keyData === \"g\" || keyData === \"\\x1b[H\") {\n\t\t\t// g or Home → top\n\t\t\tthis.scrollOffset = 0;\n\t\t} else if (keyData === \"G\" || keyData === \"\\x1b[F\") {\n\t\t\t// G or End → bottom\n\t\t\tthis.scrollOffset = this.styledLines.length;\n\t\t} else if (keyData === \"e\") {\n\t\t\tthis.callbacks.onOpenEditor();\n\t\t\treturn;\n\t\t} else if (kb.matches(keyData, \"tui.select.cancel\") || keyData === \"q\") {\n\t\t\tthis.callbacks.onClose();\n\t\t\treturn;\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.clampScroll(viewport);\n\t\tthis.tui.requestRender();\n\t}\n}\n"]}
@@ -20,6 +20,8 @@ export interface SettingsConfig {
20
20
  collapseChangelog: boolean;
21
21
  smartRouting: boolean;
22
22
  codebaseMemory: boolean;
23
+ autoSkills: boolean;
24
+ diffInEditor: boolean;
23
25
  doubleEscapeAction: "fork" | "tree" | "none";
24
26
  treeFilterMode: "default" | "no-tools" | "user-only" | "labeled-only" | "all";
25
27
  showHardwareCursor: boolean;
@@ -47,6 +49,8 @@ export interface SettingsCallbacks {
47
49
  onCollapseChangelogChange: (collapsed: boolean) => void;
48
50
  onSmartRoutingChange: (enabled: boolean) => void;
49
51
  onCodebaseMemoryChange: (enabled: boolean) => void;
52
+ onAutoSkillsChange: (enabled: boolean) => void;
53
+ onDiffInEditorChange: (enabled: boolean) => void;
50
54
  onDoubleEscapeActionChange: (action: "fork" | "tree" | "none") => void;
51
55
  onTreeFilterModeChange: (mode: "default" | "no-tools" | "user-only" | "labeled-only" | "all") => void;
52
56
  onShowHardwareCursorChange: (enabled: boolean) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"settings-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/settings-selector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"settings-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/settings-selector.ts"],"names":[],"mappings":"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"}
@@ -140,6 +140,20 @@ export class SettingsSelectorComponent extends Container {
140
140
  currentValue: config.codebaseMemory ? "true" : "false",
141
141
  values: ["true", "false"],
142
142
  },
143
+ {
144
+ id: "auto-skills",
145
+ label: "Auto-save skills",
146
+ description: "Let the agent save learned skills without asking for approval each time",
147
+ currentValue: config.autoSkills ? "true" : "false",
148
+ values: ["true", "false"],
149
+ },
150
+ {
151
+ id: "diff-in-editor",
152
+ label: "Diff in editor",
153
+ description: "Open /diff output in your $EDITOR instead of inline in the chat",
154
+ currentValue: config.diffInEditor ? "true" : "false",
155
+ values: ["true", "false"],
156
+ },
143
157
  {
144
158
  id: "collapse-changelog",
145
159
  label: "Collapse changelog",
@@ -342,6 +356,12 @@ export class SettingsSelectorComponent extends Container {
342
356
  case "codebase-memory":
343
357
  callbacks.onCodebaseMemoryChange(newValue === "true");
344
358
  break;
359
+ case "auto-skills":
360
+ callbacks.onAutoSkillsChange(newValue === "true");
361
+ break;
362
+ case "diff-in-editor":
363
+ callbacks.onDiffInEditorChange(newValue === "true");
364
+ break;
345
365
  case "collapse-changelog":
346
366
  callbacks.onCollapseChangelogChange(newValue === "true");
347
367
  break;