diff --git a/docs/docs/assets/images/skills/tui-skill-browser.png b/docs/docs/assets/images/skills/tui-skill-browser.png new file mode 100644 index 0000000000..d8ba0085e7 Binary files /dev/null and b/docs/docs/assets/images/skills/tui-skill-browser.png differ diff --git a/docs/docs/assets/images/skills/tui-skill-create.png b/docs/docs/assets/images/skills/tui-skill-create.png new file mode 100644 index 0000000000..bcfb4814c8 Binary files /dev/null and b/docs/docs/assets/images/skills/tui-skill-create.png differ diff --git a/docs/docs/assets/images/skills/tui-skill-install.png b/docs/docs/assets/images/skills/tui-skill-install.png new file mode 100644 index 0000000000..bb9bc04dfd Binary files /dev/null and b/docs/docs/assets/images/skills/tui-skill-install.png differ diff --git a/docs/docs/configure/skills.md b/docs/docs/configure/skills.md index 9a55caa6dd..259c1fe775 100644 --- a/docs/docs/configure/skills.md +++ b/docs/docs/configure/skills.md @@ -120,16 +120,28 @@ altimate-code skill remove my-tool # remove skill + paired tool ### TUI -Type `/skills` in the TUI prompt to open the skill browser. From there: +Open the skill browser with `ctrl+i` when no other dialog is open, or type `/skills` in the prompt: + +![Skill Browser](../assets/images/skills/tui-skill-browser.png) + +**Keyboard shortcuts:** | Key | Action | |-----|--------| +| `ctrl+i` | Open skill browser (when no dialog is open) / Install skill (when inside browser) | | Enter | Use — inserts `/` into the prompt | | `ctrl+a` | Actions — show, edit, test, or remove the selected skill | | `ctrl+n` | New — scaffold a new skill + CLI tool | -| `ctrl+i` | Install — install skills from a GitHub repo or URL | | Esc | Back — returns to previous screen | +**Create skill** (`ctrl+n`): + +![Create Skill Dialog](../assets/images/skills/tui-skill-create.png) + +**Install skill** (`ctrl+i` inside browser): + +![Install Skill Dialog](../assets/images/skills/tui-skill-install.png) + ## Adding Custom Skills The fastest way to create a custom skill is with the scaffolder: diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx index 4cd3832b67..136e52ef99 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx @@ -258,11 +258,14 @@ function DialogSkillCreate() { placeholder="my-tool" onConfirm={async (rawName) => { const name = rawName.trim() - dialog.clear() if (!name) { + dialog.clear() toast.show({ message: "No name provided.", variant: "error", duration: 4000 }) return } + // Close dialog after validation but before async work to avoid premature + // onClose callback triggering reopenSkillList during the operation + dialog.clear() toast.show({ message: `Creating "${name}"...`, variant: "info", duration: 30000 }) try { const result = await createSkillDirect(name, gitRoot(sdk.directory ?? process.cwd())) @@ -289,7 +292,6 @@ function DialogSkillCreate() { toast.show({ message: `Create error: ${msg.slice(0, 200)}`, variant: "error", duration: 8000 }) } }} - onCancel={() => dialog.clear()} /> ) } @@ -306,11 +308,13 @@ function DialogSkillInstall() { onConfirm={async (rawSource) => { // Strip trailing dots, whitespace, and .git suffix that users might paste const source = rawSource.trim().replace(/\.+$/, "").replace(/\.git$/, "") - dialog.clear() if (!source) { + dialog.clear() toast.show({ message: "No source provided.", variant: "error", duration: 4000 }) return } + // Close dialog after validation to avoid premature onClose callback + dialog.clear() const progress = (status: string) => { toast.show({ message: `Installing from ${source}\n\n${status}`, variant: "info", duration: 600000 }) } @@ -341,7 +345,6 @@ function DialogSkillInstall() { toast.show({ message: `Install error: ${msg.slice(0, 200)}`, variant: "error", duration: 8000 }) } }} - onCancel={() => dialog.clear()} /> ) } @@ -525,14 +528,22 @@ export function DialogSkill(props: DialogSkillProps) { keybind: Keybind.parse("ctrl+n")[0], title: "new", onTrigger: async () => { - dialog.replace(() => ) + dialog.replace( + () => , + // defer to next tick so dialog stack is fully cleared before reopening + () => setTimeout(() => reopenSkillList(), 0), + ) }, }, { keybind: Keybind.parse("ctrl+i")[0], title: "install", onTrigger: async () => { - dialog.replace(() => ) + dialog.replace( + () => , + // defer to next tick so dialog stack is fully cleared before reopening + () => setTimeout(() => reopenSkillList(), 0), + ) }, }, ]) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 93d7016d37..0f715255ca 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -390,6 +390,9 @@ export function Prompt(props: PromptProps) { title: "Skills", value: "prompt.skills", category: "Prompt", + // altimate_change start — global keybind to open skills dialog + keybind: "skill_list", + // altimate_change end slash: { name: "skills", }, diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 12e3730e43..a19a18379c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -881,6 +881,9 @@ export namespace Config { model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"), command_list: z.string().optional().default("ctrl+p").describe("List available commands"), agent_list: z.string().optional().default("a").describe("List agents"), + // altimate_change start — global keybind to open skills dialog + skill_list: z.string().optional().default("ctrl+i").describe("Open skill browser"), + // altimate_change end agent_cycle: z.string().optional().default("tab").describe("Next agent"), agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), diff --git a/packages/opencode/test/altimate/training-import.test.ts b/packages/opencode/test/altimate/training-import.test.ts index bd42d7bc08..40a6941070 100644 --- a/packages/opencode/test/altimate/training-import.test.ts +++ b/packages/opencode/test/altimate/training-import.test.ts @@ -48,16 +48,14 @@ function setupMocks(opts: { saveSpy?.mockRestore() budgetSpy?.mockRestore() - readFileSpy = spyOn(fs, "readFile").mockImplementation(async () => opts.fileContent as any) + readFileSpy = spyOn(fs, "readFile").mockImplementation(async () => Buffer.from(opts.fileContent) as any) countSpy = spyOn(TrainingStore, "count").mockImplementation(async () => ({ standard: opts.currentCount ?? 0, glossary: opts.currentCount ?? 0, playbook: opts.currentCount ?? 0, - context: opts.currentCount ?? 0, rule: opts.currentCount ?? 0, pattern: opts.currentCount ?? 0, context: opts.currentCount ?? 0, - rule: opts.currentCount ?? 0, })) saveSpy = spyOn(TrainingStore, "save").mockImplementation(async () => { if (opts.saveShouldFail) throw new Error("store write failed")