Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 14 additions & 2 deletions docs/docs/configure/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `/<skill-name>` 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:
Expand Down
23 changes: 17 additions & 6 deletions packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand All @@ -289,7 +292,6 @@ function DialogSkillCreate() {
toast.show({ message: `Create error: ${msg.slice(0, 200)}`, variant: "error", duration: 8000 })
}
}}
onCancel={() => dialog.clear()}
/>
)
}
Expand All @@ -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 })
}
Expand Down Expand Up @@ -341,7 +345,6 @@ function DialogSkillInstall() {
toast.show({ message: `Install error: ${msg.slice(0, 200)}`, variant: "error", duration: 8000 })
}
}}
onCancel={() => dialog.clear()}
/>
)
}
Expand Down Expand Up @@ -525,14 +528,22 @@ export function DialogSkill(props: DialogSkillProps) {
keybind: Keybind.parse("ctrl+n")[0],
title: "new",
onTrigger: async () => {
dialog.replace(() => <DialogSkillCreate />)
dialog.replace(
() => <DialogSkillCreate />,
// 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(() => <DialogSkillInstall />)
dialog.replace(
() => <DialogSkillInstall />,
// defer to next tick so dialog stack is fully cleared before reopening
() => setTimeout(() => reopenSkillList(), 0),
)
},
},
])
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<leader>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"),
Expand Down
4 changes: 1 addition & 3 deletions packages/opencode/test/altimate/training-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,14 @@
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")
Expand Down Expand Up @@ -97,7 +95,7 @@
{ file_path: "style-guide.md", kind: "standard", scope: "project", dry_run: true, max_entries: 20 },
ctx,
)
expect(result.metadata.success).toBe(true)

Check failure on line 98 in packages/opencode/test/altimate/training-import.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

error: expect(received).toBe(expected)

Expected: true Received: false at <anonymous> (/home/runner/work/altimate-code/altimate-code/packages/opencode/test/altimate/training-import.test.ts:98:37)
expect(result.metadata.dry_run).toBe(true)
expect(result.metadata.count).toBe(2)
expect(result.output).toContain("naming-conventions")
Expand All @@ -119,7 +117,7 @@
{ file_path: "doc.md", kind: "standard", scope: "project", dry_run: true, max_entries: 20 },
ctx,
)
expect(result.metadata.count).toBe(1)

Check failure on line 120 in packages/opencode/test/altimate/training-import.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

error: expect(received).toBe(expected)

Expected: 1 Received: 0 at <anonymous> (/home/runner/work/altimate-code/altimate-code/packages/opencode/test/altimate/training-import.test.ts:120:35)
// The H1 title should appear as context inside the entry
expect(result.output).toContain("Context: Data Engineering Standards")
})
Expand All @@ -140,7 +138,7 @@
{ file_path: "empty.md", kind: "glossary", scope: "project", dry_run: true, max_entries: 20 },
ctx,
)
expect(result.title).toContain("NO SECTIONS")

Check failure on line 141 in packages/opencode/test/altimate/training-import.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

error: expect(received).toContain(expected)

Expected to contain: "NO SECTIONS" Received: "Import: ERROR" at <anonymous> (/home/runner/work/altimate-code/altimate-code/packages/opencode/test/altimate/training-import.test.ts:141:26)
expect(result.metadata.success).toBe(false)
expect(result.metadata.count).toBe(0)
})
Expand All @@ -164,7 +162,7 @@
{ file_path: "mixed.md", kind: "standard", scope: "project", dry_run: true, max_entries: 20 },
ctx,
)
expect(result.metadata.count).toBe(2)

Check failure on line 165 in packages/opencode/test/altimate/training-import.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

error: expect(received).toBe(expected)

Expected: 2 Received: 0 at <anonymous> (/home/runner/work/altimate-code/altimate-code/packages/opencode/test/altimate/training-import.test.ts:165:35)
expect(result.output).toContain("section-with-content")
// Empty section is included with 0 chars after trim
expect(result.output).toContain("empty-section")
Expand All @@ -181,7 +179,7 @@
{ file_path: "many.md", kind: "standard", scope: "project", dry_run: true, max_entries: 3 },
ctx,
)
expect(result.metadata.count).toBe(3)

Check failure on line 182 in packages/opencode/test/altimate/training-import.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

error: expect(received).toBe(expected)

Expected: 3 Received: 0 at <anonymous> (/home/runner/work/altimate-code/altimate-code/packages/opencode/test/altimate/training-import.test.ts:182:35)
})
})

Expand All @@ -207,7 +205,7 @@
ctx,
)
// Only 2 slots available, 3 entries found — should show WARNING
expect(result.metadata.count).toBe(2)

Check failure on line 208 in packages/opencode/test/altimate/training-import.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

error: expect(received).toBe(expected)

Expected: 2 Received: 0 at <anonymous> (/home/runner/work/altimate-code/altimate-code/packages/opencode/test/altimate/training-import.test.ts:208:35)
expect(result.output).toContain("WARNING")
expect(result.output).toContain("SKIP")
})
Expand All @@ -230,7 +228,7 @@
{ file_path: "guide.md", kind: "standard", scope: "project", dry_run: false, max_entries: 20 },
ctx,
)
expect(result.metadata.success).toBe(true)

Check failure on line 231 in packages/opencode/test/altimate/training-import.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

error: expect(received).toBe(expected)

Expected: true Received: false at <anonymous> (/home/runner/work/altimate-code/altimate-code/packages/opencode/test/altimate/training-import.test.ts:231:37)
expect(result.metadata.count).toBe(2)
expect(saveSpy).toHaveBeenCalledTimes(2)
expect(result.output).toContain("Imported 2")
Expand All @@ -251,7 +249,7 @@
{ file_path: "fail.md", kind: "standard", scope: "project", dry_run: false, max_entries: 20 },
ctx,
)
expect(result.metadata.success).toBe(true) // tool itself succeeds

Check failure on line 252 in packages/opencode/test/altimate/training-import.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

error: expect(received).toBe(expected)

Expected: true Received: false at <anonymous> (/home/runner/work/altimate-code/altimate-code/packages/opencode/test/altimate/training-import.test.ts:252:37)
expect(result.metadata.count).toBe(0) // but no entries saved
expect(result.metadata.skipped).toBe(1)
expect(result.output).toContain("FAIL")
Expand All @@ -272,7 +270,7 @@
{ file_path: "special.md", kind: "standard", scope: "project", dry_run: true, max_entries: 20 },
ctx,
)
expect(result.metadata.count).toBe(1)

Check failure on line 273 in packages/opencode/test/altimate/training-import.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

error: expect(received).toBe(expected)

Expected: 1 Received: 0 at <anonymous> (/home/runner/work/altimate-code/altimate-code/packages/opencode/test/altimate/training-import.test.ts:273:35)
// Slugified name should strip special chars and use hyphens
expect(result.output).toContain("cte-best-practices-v20-updated")
})
Expand Down
Loading