Skip to content

Commit e889bf8

Browse files
committed
Enhance publish UI with dependency direction visualization
- Add optional dependents (parent agents) to publish flow with toggle - Redesign confirmation screen with vertical tree and directional arrows - Filter bundled agents from publish list (isBundled flag) - Escape key clears search first, then exits publish mode - Add terminal too-small detection with helpful message - Clean up code: optional title prop, rename agents→allAgents for clarity
1 parent 99e4f7f commit e889bf8

File tree

17 files changed

+760
-261
lines changed

17 files changed

+760
-261
lines changed

cli/scripts/prebuild-agents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export function getBundledAgentsAsLocalInfo(): LocalAgentInfo[] {
115115
id: agent.id,
116116
displayName: agent.displayName || agent.id,
117117
filePath: '[bundled]',
118+
isBundled: true,
118119
}));
119120
}
120121
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import { getAllPublishAgentIds } from '../../components/publish-confirmation'
4+
5+
import type { LocalAgentInfo } from '../../utils/local-agent-registry'
6+
7+
const makeAgent = (id: string, isBundled = false): LocalAgentInfo => ({
8+
id,
9+
displayName: id,
10+
filePath: `/agents/${id}.ts`,
11+
isBundled,
12+
})
13+
14+
describe('getAllPublishAgentIds', () => {
15+
test('ignores bundled agents even if selected', () => {
16+
const base = makeAgent('base', true)
17+
const userA = makeAgent('user-a')
18+
const agents = [base, userA]
19+
const defs = new Map<string, { spawnableAgents?: string[] }>()
20+
21+
const ids = getAllPublishAgentIds([base, userA], agents, defs)
22+
23+
expect(ids).toEqual(['user-a'])
24+
})
25+
26+
test('does not include bundled dependencies discovered via spawns', () => {
27+
const base = makeAgent('base', true)
28+
const userA = makeAgent('user-a')
29+
const agents = [base, userA]
30+
const defs = new Map<string, { spawnableAgents?: string[] }>([
31+
['user-a', { spawnableAgents: ['base'] }],
32+
])
33+
34+
const ids = getAllPublishAgentIds([userA], agents, defs)
35+
36+
expect(ids).toEqual(['user-a'])
37+
})
38+
39+
test('only adds publishable dependents when includeDependents is true', () => {
40+
const base = makeAgent('base', true)
41+
const userA = makeAgent('user-a')
42+
const userB = makeAgent('user-b')
43+
const agents = [base, userA, userB]
44+
const defs = new Map<string, { spawnableAgents?: string[] }>([
45+
['user-a', { spawnableAgents: [] }],
46+
['user-b', { spawnableAgents: ['user-a'] }],
47+
['base', { spawnableAgents: ['user-a'] }],
48+
])
49+
50+
const ids = getAllPublishAgentIds([userA], agents, defs, true)
51+
52+
expect(ids).toEqual(['user-a', 'user-b'])
53+
})
54+
55+
test('includes transitive dependents when includeDependents is true', () => {
56+
const userA = makeAgent('user-a')
57+
const userB = makeAgent('user-b')
58+
const userC = makeAgent('user-c')
59+
const agents = [userA, userB, userC]
60+
const defs = new Map<string, { spawnableAgents?: string[] }>([
61+
['user-a', { spawnableAgents: [] }],
62+
['user-b', { spawnableAgents: ['user-a'] }],
63+
['user-c', { spawnableAgents: ['user-b'] }],
64+
])
65+
66+
const ids = getAllPublishAgentIds([userA], agents, defs, true)
67+
68+
expect(ids).toEqual(['user-a', 'user-b', 'user-c'])
69+
})
70+
})

cli/src/commands/publish.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ export interface PublishResult {
2828
async function publishAgentTemplates(
2929
data: Record<string, any>[],
3030
authToken: string,
31+
allLocalAgentIds: string[],
3132
): Promise<PublishAgentsResponse & { statusCode?: number }> {
3233
setApiClientAuthToken(authToken)
3334
const apiClient = getApiClient()
3435

3536
try {
36-
const response = await apiClient.publish(data)
37+
const response = await apiClient.publish(data, allLocalAgentIds)
3738

3839
if (!response.ok) {
3940
// Try to use the full error data if available (includes details, hint, etc.)
@@ -159,16 +160,21 @@ export async function handlePublish(agentIds: string[]): Promise<PublishResult>
159160
matchingTemplates[matchingTemplate.id] = processedTemplate
160161
}
161162

163+
// Get all local agent IDs so the server knows which agents exist locally
164+
// (even if not being published) for validation purposes
165+
const allLocalAgentIds = loadedDefinitions.map((template) => template.id)
166+
162167
const result = await publishAgentTemplates(
163168
Object.values(matchingTemplates),
164169
user.authToken!,
170+
allLocalAgentIds,
165171
)
166172

167173
if (result.success) {
168174
return {
169175
success: true,
170176
publisherId: result.publisherId,
171-
agents: result.agents,
177+
agents: result.agents ?? [],
172178
}
173179
}
174180

cli/src/components/agent-checklist.tsx

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { TextAttributes } from '@opentui/core'
22
import React, { useMemo, useRef, useEffect, useState } from 'react'
33

4+
import { pluralize } from '@codebuff/common/util/string'
5+
46
import { Button } from './button'
57
import { useTheme } from '../hooks/use-theme'
68
import { getSimpleAgentId } from '../utils/agent-id-utils'
@@ -108,7 +110,10 @@ const DepTree: React.FC<{
108110
}
109111

110112
interface AgentChecklistProps {
111-
agents: LocalAgentInfo[]
113+
/** All agents (used for dependency tree calculations) */
114+
allAgents: LocalAgentInfo[]
115+
/** Agents filtered by search query (displayed in the list) */
116+
filteredAgents: LocalAgentInfo[]
112117
selectedIds: Set<string>
113118
searchQuery: string
114119
focusedIndex: number
@@ -119,7 +124,8 @@ interface AgentChecklistProps {
119124
}
120125

121126
export const AgentChecklist: React.FC<AgentChecklistProps> = ({
122-
agents,
127+
allAgents,
128+
filteredAgents,
123129
selectedIds,
124130
searchQuery,
125131
focusedIndex,
@@ -135,17 +141,17 @@ export const AgentChecklist: React.FC<AgentChecklistProps> = ({
135141
const [hoveredSubagentLink, setHoveredSubagentLink] = useState<string | null>(null)
136142

137143
// Precompute local agent IDs for dependency calculations
138-
const localAgentIds = useMemo(() => new Set(agents.map((a) => a.id)), [agents])
144+
const localAgentIds = useMemo(() => new Set(allAgents.map((a) => a.id)), [allAgents])
139145

140146
// Calculate dependency count for each agent
141147
const dependencyCounts = useMemo(() => {
142148
const counts = new Map<string, number>()
143-
for (const agent of agents) {
149+
for (const agent of allAgents) {
144150
const count = countDependencies(agent.id, agentDefinitions, localAgentIds, new Set())
145151
counts.set(agent.id, count)
146152
}
147153
return counts
148-
}, [agents, agentDefinitions, localAgentIds])
154+
}, [allAgents, agentDefinitions, localAgentIds])
149155

150156
// Toggle expansion of an agent's dependencies
151157
const toggleExpanded = (agentId: string) => {
@@ -160,19 +166,6 @@ export const AgentChecklist: React.FC<AgentChecklistProps> = ({
160166
})
161167
}
162168

163-
// Filter agents based on search query (instant filter)
164-
const filteredAgents = useMemo(() => {
165-
if (!searchQuery.trim()) {
166-
return agents
167-
}
168-
const query = searchQuery.toLowerCase()
169-
return agents.filter(
170-
(agent) =>
171-
agent.displayName.toLowerCase().includes(query) ||
172-
agent.id.toLowerCase().includes(query),
173-
)
174-
}, [agents, searchQuery])
175-
176169
// Scroll focused item into view when focus changes via keyboard
177170
useEffect(() => {
178171
const scrollbox = scrollRef.current
@@ -244,6 +237,7 @@ export const AgentChecklist: React.FC<AgentChecklistProps> = ({
244237
const depCount = dependencyCounts.get(agent.id) ?? 0
245238
const isExpanded = expandedAgentIds.has(agent.id)
246239
const isSubagentLinkHovered = hoveredSubagentLink === agent.id
240+
const subagentLabel = `(${isExpanded ? '-' : '+'} ${pluralize(depCount, 'subagent')})`
247241

248242
const symbol = isSelected
249243
? SYMBOLS.CHECKBOX_CHECKED
@@ -333,7 +327,7 @@ export const AgentChecklist: React.FC<AgentChecklistProps> = ({
333327
: undefined,
334328
}}
335329
>
336-
{isExpanded ? `(- ${depCount} subagent${depCount === 1 ? '' : 's'})` : `(+ ${depCount} subagent${depCount === 1 ? '' : 's'})`}
330+
{subagentLabel}
337331
</text>
338332
</Button>
339333
)}
@@ -342,7 +336,7 @@ export const AgentChecklist: React.FC<AgentChecklistProps> = ({
342336
{/* Expanded dependency tree */}
343337
{isExpanded && depCount > 0 && (
344338
<DepTree
345-
nodes={buildDepTree(agent.id, agents, agentDefinitions, localAgentIds, new Set())}
339+
nodes={buildDepTree(agent.id, allAgents, agentDefinitions, localAgentIds, new Set())}
346340
depth={0}
347341
theme={theme}
348342
/>
@@ -351,15 +345,6 @@ export const AgentChecklist: React.FC<AgentChecklistProps> = ({
351345
)
352346
})}
353347
</scrollbox>
354-
355-
{/* Selection count */}
356-
<box style={{ marginTop: 1, marginLeft: 1 }}>
357-
<text style={{ fg: theme.secondary }}>
358-
{selectedIds.size === 0
359-
? 'No agents selected'
360-
: `Selected: ${Array.from(selectedIds).join(', ')}`}
361-
</text>
362-
</box>
363348
</box>
364349
)
365350
}

0 commit comments

Comments
 (0)