Skip to content

Commit 8d5804f

Browse files
anandgupta42claude
andauthored
fix: skill builder ctrl+i keybind, ESC navigation, docs screenshots (#386)
* fix: [AI-385] skill builder `ctrl+i` global keybind, ESC navigation, and docs screenshots - Add `skill_list` keybind (default `ctrl+i`) to open skill browser globally - Fix ESC in Create/Install sub-dialogs to return to skill list instead of main - Remove dead `onCancel` callbacks from `DialogSkillCreate` and `DialogSkillInstall` - Add TUI screenshots to skills documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address code review feedback — docs wording, dialog.clear() timing, comments - Narrow ctrl+i docs scope: "when no other dialog is open" (CodeRabbit) - Merge duplicate ctrl+i rows in keybind table (consensus review) - Move dialog.clear() after validation in Create/Install onConfirm (Gemini) - Add explanatory comments for setTimeout tick deferral pattern Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: correct TypeScript errors in `training-import.test.ts` mock types - Fix `TrainingStore.count` mock: replace invalid `naming` key with `rule`, add missing `context` - Fix `fs.readFile` mock: return `Buffer.from()` instead of raw string Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 27941cf commit 8d5804f

File tree

8 files changed

+38
-11
lines changed

8 files changed

+38
-11
lines changed
103 KB
Loading
48.4 KB
Loading
139 KB
Loading

docs/docs/configure/skills.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,28 @@ altimate-code skill remove my-tool # remove skill + paired tool
120120

121121
### TUI
122122

123-
Type `/skills` in the TUI prompt to open the skill browser. From there:
123+
Open the skill browser with `ctrl+i` when no other dialog is open, or type `/skills` in the prompt:
124+
125+
![Skill Browser](../assets/images/skills/tui-skill-browser.png)
126+
127+
**Keyboard shortcuts:**
124128

125129
| Key | Action |
126130
|-----|--------|
131+
| `ctrl+i` | Open skill browser (when no dialog is open) / Install skill (when inside browser) |
127132
| Enter | Use — inserts `/<skill-name>` into the prompt |
128133
| `ctrl+a` | Actions — show, edit, test, or remove the selected skill |
129134
| `ctrl+n` | New — scaffold a new skill + CLI tool |
130-
| `ctrl+i` | Install — install skills from a GitHub repo or URL |
131135
| Esc | Back — returns to previous screen |
132136

137+
**Create skill** (`ctrl+n`):
138+
139+
![Create Skill Dialog](../assets/images/skills/tui-skill-create.png)
140+
141+
**Install skill** (`ctrl+i` inside browser):
142+
143+
![Install Skill Dialog](../assets/images/skills/tui-skill-install.png)
144+
133145
## Adding Custom Skills
134146

135147
The fastest way to create a custom skill is with the scaffolder:

packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,14 @@ function DialogSkillCreate() {
258258
placeholder="my-tool"
259259
onConfirm={async (rawName) => {
260260
const name = rawName.trim()
261-
dialog.clear()
262261
if (!name) {
262+
dialog.clear()
263263
toast.show({ message: "No name provided.", variant: "error", duration: 4000 })
264264
return
265265
}
266+
// Close dialog after validation but before async work to avoid premature
267+
// onClose callback triggering reopenSkillList during the operation
268+
dialog.clear()
266269
toast.show({ message: `Creating "${name}"...`, variant: "info", duration: 30000 })
267270
try {
268271
const result = await createSkillDirect(name, gitRoot(sdk.directory ?? process.cwd()))
@@ -289,7 +292,6 @@ function DialogSkillCreate() {
289292
toast.show({ message: `Create error: ${msg.slice(0, 200)}`, variant: "error", duration: 8000 })
290293
}
291294
}}
292-
onCancel={() => dialog.clear()}
293295
/>
294296
)
295297
}
@@ -306,11 +308,13 @@ function DialogSkillInstall() {
306308
onConfirm={async (rawSource) => {
307309
// Strip trailing dots, whitespace, and .git suffix that users might paste
308310
const source = rawSource.trim().replace(/\.+$/, "").replace(/\.git$/, "")
309-
dialog.clear()
310311
if (!source) {
312+
dialog.clear()
311313
toast.show({ message: "No source provided.", variant: "error", duration: 4000 })
312314
return
313315
}
316+
// Close dialog after validation to avoid premature onClose callback
317+
dialog.clear()
314318
const progress = (status: string) => {
315319
toast.show({ message: `Installing from ${source}\n\n${status}`, variant: "info", duration: 600000 })
316320
}
@@ -341,7 +345,6 @@ function DialogSkillInstall() {
341345
toast.show({ message: `Install error: ${msg.slice(0, 200)}`, variant: "error", duration: 8000 })
342346
}
343347
}}
344-
onCancel={() => dialog.clear()}
345348
/>
346349
)
347350
}
@@ -525,14 +528,22 @@ export function DialogSkill(props: DialogSkillProps) {
525528
keybind: Keybind.parse("ctrl+n")[0],
526529
title: "new",
527530
onTrigger: async () => {
528-
dialog.replace(() => <DialogSkillCreate />)
531+
dialog.replace(
532+
() => <DialogSkillCreate />,
533+
// defer to next tick so dialog stack is fully cleared before reopening
534+
() => setTimeout(() => reopenSkillList(), 0),
535+
)
529536
},
530537
},
531538
{
532539
keybind: Keybind.parse("ctrl+i")[0],
533540
title: "install",
534541
onTrigger: async () => {
535-
dialog.replace(() => <DialogSkillInstall />)
542+
dialog.replace(
543+
() => <DialogSkillInstall />,
544+
// defer to next tick so dialog stack is fully cleared before reopening
545+
() => setTimeout(() => reopenSkillList(), 0),
546+
)
536547
},
537548
},
538549
])

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ export function Prompt(props: PromptProps) {
390390
title: "Skills",
391391
value: "prompt.skills",
392392
category: "Prompt",
393+
// altimate_change start — global keybind to open skills dialog
394+
keybind: "skill_list",
395+
// altimate_change end
393396
slash: {
394397
name: "skills",
395398
},

packages/opencode/src/config/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,9 @@ export namespace Config {
881881
model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
882882
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
883883
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
884+
// altimate_change start — global keybind to open skills dialog
885+
skill_list: z.string().optional().default("ctrl+i").describe("Open skill browser"),
886+
// altimate_change end
884887
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
885888
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
886889
variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),

packages/opencode/test/altimate/training-import.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,14 @@ function setupMocks(opts: {
4848
saveSpy?.mockRestore()
4949
budgetSpy?.mockRestore()
5050

51-
readFileSpy = spyOn(fs, "readFile").mockImplementation(async () => opts.fileContent as any)
51+
readFileSpy = spyOn(fs, "readFile").mockImplementation(async () => Buffer.from(opts.fileContent) as any)
5252
countSpy = spyOn(TrainingStore, "count").mockImplementation(async () => ({
5353
standard: opts.currentCount ?? 0,
5454
glossary: opts.currentCount ?? 0,
5555
playbook: opts.currentCount ?? 0,
56-
context: opts.currentCount ?? 0,
5756
rule: opts.currentCount ?? 0,
5857
pattern: opts.currentCount ?? 0,
5958
context: opts.currentCount ?? 0,
60-
rule: opts.currentCount ?? 0,
6159
}))
6260
saveSpy = spyOn(TrainingStore, "save").mockImplementation(async () => {
6361
if (opts.saveShouldFail) throw new Error("store write failed")

0 commit comments

Comments
 (0)