Skip to content

Commit 37588f0

Browse files
committed
Initial impl
1 parent 840494d commit 37588f0

File tree

23 files changed

+884
-35
lines changed

23 files changed

+884
-35
lines changed

bun.lock

Lines changed: 16 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/chat.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { MessageWithAgents } from './components/message-with-agents'
2121
import { PendingBashMessage } from './components/pending-bash-message'
2222
import { StatusBar } from './components/status-bar'
2323
import { TopBanner } from './components/top-banner'
24-
import { SLASH_COMMANDS } from './data/slash-commands'
24+
import { getSlashCommandsWithSkills } from './data/slash-commands'
2525
import { useAgentValidation } from './hooks/use-agent-validation'
2626
import { useAskUserBridge } from './hooks/use-ask-user-bridge'
2727
import { useChatInput } from './hooks/use-chat-input'
@@ -63,6 +63,7 @@ import {
6363
createDefaultChatKeyboardState,
6464
} from './utils/keyboard-actions'
6565
import { loadLocalAgents } from './utils/local-agent-registry'
66+
import { getLoadedSkills } from './utils/skill-registry'
6667
import {
6768
getStatusIndicatorState,
6869
type AuthStatus,
@@ -205,15 +206,20 @@ export const Chat = ({
205206
const setInputMode = useChatStore((state) => state.setInputMode)
206207
const askUserState = useChatStore((state) => state.askUserState)
207208

209+
// Get loaded skills for slash commands
210+
const loadedSkills = useMemo(() => getLoadedSkills(), [])
211+
208212
// Filter slash commands based on current ads state - only show the option that changes state
213+
// Also merge in skill commands
209214
const filteredSlashCommands = useMemo(() => {
210215
const adsEnabled = getAdsEnabled()
211-
return SLASH_COMMANDS.filter((cmd) => {
216+
const allCommands = getSlashCommandsWithSkills(loadedSkills)
217+
return allCommands.filter((cmd) => {
212218
if (cmd.id === 'ads:enable') return !adsEnabled
213219
if (cmd.id === 'ads:disable') return adsEnabled
214220
return true
215221
})
216-
}, [inputValue]) // Re-evaluate when input changes (user may have just toggled)
222+
}, [inputValue, loadedSkills]) // Re-evaluate when input changes (user may have just toggled)
217223

218224
const {
219225
slashContext,

cli/src/commands/command-registry.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useLoginStore } from '../state/login-store'
1515
import { capturePendingAttachments } from '../utils/pending-attachments'
1616
import { AGENT_MODES } from '../utils/constants'
1717
import { getSystemMessage, getUserMessage } from '../utils/message-history'
18+
import { getSkillByName } from '../utils/skill-registry'
1819

1920
import type { MultilineInputHandle } from '../components/multiline-input'
2021
import type { InputValue, PendingAttachment } from '../state/chat-store'
@@ -490,7 +491,77 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
490491

491492
export function findCommand(cmd: string): CommandDefinition | undefined {
492493
const lowerCmd = cmd.toLowerCase()
493-
return COMMAND_REGISTRY.find(
494+
495+
// First check the static command registry
496+
const staticCommand = COMMAND_REGISTRY.find(
494497
(def) => def.name === lowerCmd || def.aliases.includes(lowerCmd),
495498
)
499+
if (staticCommand) {
500+
return staticCommand
501+
}
502+
503+
// Check if this is a skill command
504+
const skill = getSkillByName(lowerCmd)
505+
if (skill) {
506+
return createSkillCommand(skill.name)
507+
}
508+
509+
return undefined
510+
}
511+
512+
/**
513+
* Creates a dynamic command definition for a skill.
514+
* When invoked, the skill's content is sent to the agent.
515+
*/
516+
function createSkillCommand(skillName: string): CommandDefinition {
517+
return defineCommandWithArgs({
518+
name: skillName,
519+
handler: (params, args) => {
520+
const skill = getSkillByName(skillName)
521+
if (!skill) {
522+
params.setMessages((prev) => [
523+
...prev,
524+
getUserMessage(params.inputValue.trim()),
525+
getSystemMessage(`Skill not found: ${skillName}`),
526+
])
527+
params.saveToHistory(params.inputValue.trim())
528+
params.setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
529+
return
530+
}
531+
532+
const trimmed = params.inputValue.trim()
533+
params.saveToHistory(trimmed)
534+
params.setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
535+
536+
// Build the message content with skill context and optional user args
537+
const skillContext = `<skill name="${skill.name}">
538+
${skill.content}
539+
</skill>`
540+
541+
const userPrompt = args.trim()
542+
? `${skillContext}\n\nUser request: ${args.trim()}`
543+
: `${skillContext}\n\nPlease use this skill to help me.`
544+
545+
// Check streaming/queue state
546+
if (
547+
params.isStreaming ||
548+
params.streamMessageIdRef.current ||
549+
params.isChainInProgressRef.current
550+
) {
551+
const pendingAttachments = capturePendingAttachments()
552+
params.addToQueue(userPrompt, pendingAttachments)
553+
params.setInputFocused(true)
554+
params.inputRef.current?.focus()
555+
return
556+
}
557+
558+
params.sendMessage({
559+
content: userPrompt,
560+
agentMode: params.agentMode,
561+
})
562+
setTimeout(() => {
563+
params.scrollToLatest()
564+
}, 0)
565+
},
566+
})
496567
}

cli/src/data/slash-commands.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { SkillsMap } from '@codebuff/common/types/skill'
2+
13
import { AGENT_MODES } from '../utils/constants'
24

35
export interface SlashCommand {
@@ -150,3 +152,17 @@ export const SLASHLESS_COMMAND_IDS = new Set(
150152
cmd.id.toLowerCase(),
151153
),
152154
)
155+
156+
/**
157+
* Returns SLASH_COMMANDS merged with skill commands.
158+
* Skills become slash commands that users can invoke directly.
159+
*/
160+
export function getSlashCommandsWithSkills(skills: SkillsMap): SlashCommand[] {
161+
const skillCommands: SlashCommand[] = Object.values(skills).map((skill) => ({
162+
id: skill.name,
163+
label: skill.name,
164+
description: skill.description,
165+
}))
166+
167+
return [...SLASH_COMMANDS, ...skillCommands]
168+
}

cli/src/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { getAuthTokenDetails } from './utils/auth'
2727
import { resetCodebuffClient } from './utils/codebuff-client'
2828
import { getCliEnv } from './utils/env'
2929
import { initializeAgentRegistry } from './utils/local-agent-registry'
30+
import { initializeSkillRegistry } from './utils/skill-registry'
3031
import { clearLogFile, logger } from './utils/logger'
3132
import { shouldShowProjectPicker } from './utils/project-picker'
3233
import { saveRecentProject } from './utils/recent-projects'
@@ -190,6 +191,9 @@ async function main(): Promise<void> {
190191
await initializeAgentRegistry()
191192
}
192193

194+
// Initialize skill registry (loads skills from .agents/skills)
195+
await initializeSkillRegistry()
196+
193197
// Handle publish command before rendering the app
194198
if (isPublishCommand) {
195199
const publishIndex = process.argv.indexOf('publish')

0 commit comments

Comments
 (0)