Skip to content

Commit 10d78f7

Browse files
committed
feat(cli): add /publish command with interactive agent selection UI
- Add new /publish command to publish local agents to the agent store - Create searchable multi-select agent checklist with expandable dependency trees - Show (+ N subagents) count with click-to-expand dependency visualization - Add two-step flow: selection -> confirmation with side-by-side Selected/Dependencies lists - Display success/error UI after publishing with detailed results - Create usePublishMutation hook using TanStack Query for proper state management - Add publish-store.ts for managing publish mode state - Extract shared getSimpleAgentId utility to reduce code duplication
1 parent f639a8e commit 10d78f7

File tree

14 files changed

+1586
-72
lines changed

14 files changed

+1586
-72
lines changed

cli/src/chat.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { useUsageMonitor } from './hooks/use-usage-monitor'
4545
import { getProjectRoot } from './project-files'
4646
import { useChatStore } from './state/chat-store'
4747
import { useFeedbackStore } from './state/feedback-store'
48+
import { usePublishStore } from './state/publish-store'
4849
import { addClipboardPlaceholder, addPendingImageFromFile, validateAndAddImage } from './utils/add-pending-image'
4950
import { createChatScrollAcceleration } from './utils/chat-scroll-accel'
5051
import { showClipboardMessage } from './utils/clipboard'
@@ -56,6 +57,7 @@ import {
5657
createDefaultChatKeyboardState,
5758
} from './utils/keyboard-actions'
5859
import { loadLocalAgents } from './utils/local-agent-registry'
60+
import { usePublishMutation } from './hooks/use-publish-mutation'
5961
import { buildMessageTree } from './utils/message-tree-utils'
6062
import {
6163
getStatusIndicatorState,
@@ -720,6 +722,22 @@ export const Chat = ({
720722
})),
721723
)
722724

725+
const {
726+
publishMode,
727+
openPublishMode,
728+
closePublish,
729+
preSelectAgents,
730+
} = usePublishStore(
731+
useShallow((state) => ({
732+
publishMode: state.publishMode,
733+
openPublishMode: state.openPublishMode,
734+
closePublish: state.closePublish,
735+
preSelectAgents: state.preSelectAgents,
736+
})),
737+
)
738+
739+
const publishMutation = usePublishMutation()
740+
723741
const inputValueRef = useRef(inputValue)
724742
const cursorPositionRef = useRef(cursorPosition)
725743
useEffect(() => {
@@ -773,6 +791,18 @@ export const Chat = ({
773791
handleExitFeedback()
774792
}, [closeFeedback, handleExitFeedback])
775793

794+
const handleExitPublish = useCallback(() => {
795+
closePublish()
796+
setInputFocused(true)
797+
}, [closePublish, setInputFocused])
798+
799+
const handlePublish = useCallback(
800+
async (agentIds: string[]) => {
801+
await publishMutation.mutateAsync(agentIds)
802+
},
803+
[publishMutation],
804+
)
805+
776806
// Ensure bracketed paste events target the active chat input
777807
useEffect(() => {
778808
if (feedbackMode) {
@@ -814,6 +844,16 @@ export const Chat = ({
814844
saveCurrentInput('', 0)
815845
openFeedbackForMessage(null)
816846
}
847+
848+
if (result?.openPublishMode) {
849+
if (result.preSelectAgents && result.preSelectAgents.length > 0) {
850+
// Pre-select agents and skip to confirmation
851+
preSelectAgents(result.preSelectAgents)
852+
} else {
853+
// Open selection UI
854+
openPublishMode()
855+
}
856+
}
817857
}, [
818858
abortControllerRef,
819859
agentMode,
@@ -838,6 +878,8 @@ export const Chat = ({
838878
ensureQueueActiveBeforeSubmit,
839879
saveCurrentInput,
840880
openFeedbackForMessage,
881+
openPublishMode,
882+
preSelectAgents,
841883
])
842884

843885
const totalMentionMatches = agentMatches.length + fileMatches.length
@@ -1295,6 +1337,9 @@ export const Chat = ({
12951337
isNarrowWidth={isNarrowWidth}
12961338
feedbackMode={feedbackMode}
12971339
handleExitFeedback={handleExitFeedback}
1340+
publishMode={publishMode}
1341+
handleExitPublish={handleExitPublish}
1342+
handlePublish={handlePublish}
12981343
handleSubmit={handleSubmit}
12991344
onPaste={createPasteHandler({
13001345
text: inputValue,

cli/src/commands/command-registry.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export type RouterParams = {
4646
stopStreaming: () => void
4747
}
4848

49-
export type CommandResult = { openFeedbackMode?: boolean } | void
49+
export type CommandResult = { openFeedbackMode?: boolean; openPublishMode?: boolean; preSelectAgents?: string[] } | void
5050

5151
export type CommandHandler = (
5252
params: RouterParams,
@@ -265,6 +265,24 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
265265
clearInput(params)
266266
},
267267
})),
268+
{
269+
name: 'publish',
270+
aliases: [],
271+
handler: (params, args) => {
272+
const trimmedArgs = args.trim()
273+
params.saveToHistory(params.inputValue.trim())
274+
clearInput(params)
275+
276+
// If user provided agent ids directly, skip to confirmation step
277+
if (trimmedArgs) {
278+
const agentIds = trimmedArgs.split(/\s+/).filter(Boolean)
279+
return { openPublishMode: true, preSelectAgents: agentIds }
280+
}
281+
282+
// Otherwise open selection UI
283+
return { openPublishMode: true }
284+
},
285+
},
268286
]
269287

270288
export function findCommand(cmd: string): CommandDefinition | undefined {

cli/src/commands/publish.ts

Lines changed: 58 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { WEBSITE_URL } from '@codebuff/sdk'
2-
import { cyan, green, red, yellow } from 'picocolors'
32

43
import { getUserCredentials } from '../utils/auth'
54
import { getApiClient, setApiClientAuthToken } from '../utils/codebuff-api'
@@ -10,6 +9,19 @@ import type {
109
PublishAgentsResponse,
1110
} from '@codebuff/common/types/api/agents/publish'
1211

12+
export interface PublishResult {
13+
success: boolean
14+
publisherId?: string
15+
agents?: Array<{
16+
id: string
17+
version: string
18+
displayName: string
19+
}>
20+
error?: string
21+
details?: string
22+
hint?: string
23+
}
24+
1325
/**
1426
* Publish agent templates to the backend
1527
*/
@@ -77,42 +89,37 @@ async function publishAgentTemplates(
7789
/**
7890
* Handle the publish command to upload agent templates to the backend
7991
* @param agentIds The ids or display names of the agents to publish
92+
* @returns PublishResult with success/error information
8093
*/
81-
export async function handlePublish(agentIds: string[]): Promise<void> {
94+
export async function handlePublish(agentIds: string[]): Promise<PublishResult> {
8295
const user = getUserCredentials()
8396

8497
if (!user) {
85-
console.log(red('Please log in first using "login" command or web UI.'))
86-
return
98+
return {
99+
success: false,
100+
error: 'Not logged in',
101+
hint: 'Please log in first using "login" command or web UI.',
102+
}
87103
}
88104

89105
const availableAgents = getLoadedAgentsData()?.agents || []
90106

91107
if (agentIds?.length === 0) {
92-
console.log(
93-
red('Agent id is required. Usage: publish <agent-id> [agent-id2] ...'),
94-
)
95-
96-
// Show available agents
97-
if (availableAgents.length > 0) {
98-
console.log(cyan('Available agents:'))
99-
availableAgents.forEach((agent) => {
100-
const identifier =
101-
agent.displayName && agent.displayName !== agent.id
102-
? `${agent.displayName} (${agent.id})`
103-
: agent.displayName || agent.id
104-
console.log(` - ${identifier}`)
105-
})
108+
return {
109+
success: false,
110+
error: 'No agents specified',
111+
hint: 'Usage: publish <agent-id> [agent-id2] ...',
106112
}
107-
return
108113
}
109114

110115
try {
111116
const loadedDefinitions = loadAgentDefinitions()
112117

113118
if (loadedDefinitions.length === 0) {
114-
console.log(red('No valid agent templates found in .agents directory.'))
115-
return
119+
return {
120+
success: false,
121+
error: 'No valid agent templates found in .agents directory.',
122+
}
116123
}
117124

118125
const matchingTemplates: Record<string, any> = {}
@@ -125,15 +132,18 @@ export async function handlePublish(agentIds: string[]): Promise<void> {
125132
)
126133

127134
if (!matchingTemplate) {
128-
console.log(red(`Agent "${agentId}" not found. Available agents:`))
129-
availableAgents.forEach((agent) => {
130-
const identifier =
135+
const availableList = availableAgents
136+
.map((agent) =>
131137
agent.displayName && agent.displayName !== agent.id
132138
? `${agent.displayName} (${agent.id})`
133-
: agent.displayName || agent.id
134-
console.log(` - ${identifier}`)
135-
})
136-
return
139+
: agent.displayName || agent.id,
140+
)
141+
.join(', ')
142+
return {
143+
success: false,
144+
error: `Agent "${agentId}" not found`,
145+
details: `Available agents: ${availableList}`,
146+
}
137147
}
138148

139149
// Process the template for publishing
@@ -149,59 +159,38 @@ export async function handlePublish(agentIds: string[]): Promise<void> {
149159
matchingTemplates[matchingTemplate.id] = processedTemplate
150160
}
151161

152-
console.log(yellow(`Publishing:`))
153-
for (const template of Object.values(matchingTemplates)) {
154-
const displayName = (template as any).displayName || template.id
155-
console.log(` - ${displayName} (${template.id})`)
156-
}
157-
158162
const result = await publishAgentTemplates(
159163
Object.values(matchingTemplates),
160164
user.authToken!,
161165
)
162166

163167
if (result.success) {
164-
console.log(green(`✅ Successfully published:`))
165-
for (const agent of result.agents) {
166-
console.log(
167-
cyan(
168-
` - ${agent.displayName} (${result.publisherId}/${agent.id}@${agent.version})`,
169-
),
170-
)
168+
return {
169+
success: true,
170+
publisherId: result.publisherId,
171+
agents: result.agents,
171172
}
172-
return
173173
}
174174

175-
console.log(red(`❌ Failed to publish your agents`))
176-
if (result.error) console.log(red(`Error: ${result.error}`))
177-
if (result.details) console.log(red(`\n${result.details}`))
178-
if (result.hint) console.log(yellow(`\nHint: ${result.hint}`))
179-
180-
// Show helpful guidance based on error type
175+
// Build error result
176+
let hint = result.hint
181177
if (result.error?.includes('Publisher field required')) {
182-
console.log()
183-
console.log(cyan('Add a "publisher" field to your agent templates:'))
184-
console.log(yellow(' "publisher": "<publisher-id>"'))
185-
console.log()
186-
} else if (
187-
result.error?.includes('Publisher not found or not accessible')
188-
) {
189-
console.log()
190-
console.log(
191-
cyan(
192-
'Check that the publisher ID is correct and you have access to it.',
193-
),
194-
)
195-
console.log()
178+
hint = 'Add a "publisher" field to your agent templates.'
179+
} else if (result.error?.includes('Publisher not found or not accessible')) {
180+
hint = `Check that the publisher ID is correct and you have access to it. Visit ${WEBSITE_URL}/publishers to manage publishers.`
196181
}
197182

198-
console.log(cyan('Visit the website to manage your publishers:'))
199-
console.log(yellow(`${WEBSITE_URL}/publishers`))
183+
return {
184+
success: false,
185+
error: result.error,
186+
details: result.details,
187+
hint,
188+
}
200189
} catch (error) {
201-
console.log(
202-
red(
203-
`Error during publish: ${error instanceof Error ? error.message + '\n' + error.stack : String(error)}`,
204-
),
205-
)
190+
return {
191+
success: false,
192+
error: 'Publish failed',
193+
details: error instanceof Error ? error.message : String(error),
194+
}
206195
}
207196
}

0 commit comments

Comments
 (0)