Skip to content

Commit f598f41

Browse files
authored
Implement sub-agents and custom tools on sidebar editor (#1792)
1 parent 143c904 commit f598f41

File tree

31 files changed

+1406
-732
lines changed

31 files changed

+1406
-732
lines changed

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/ContentArea/index.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ export function DocumentEditorContentArea({
4545
togglePlaygroundOpen: () => void
4646
isPlaygroundTransitioning: boolean
4747
}) {
48+
const {
49+
isEnabled: isEditorSidebarEnabled,
50+
isLoading: isEditorSidebarFlagLoading,
51+
} = useFeature('editorSidebar')
52+
const { isEnabled: isRunStream } = useFeature('runs')
4853
const containerRef = useRef<HTMLDivElement | null>(null)
4954
const { metadata } = useMetadata()
5055
const { commit } = useCurrentCommit()
@@ -63,7 +68,6 @@ export function DocumentEditorContentArea({
6368
document,
6469
})
6570
const { devMode } = useDevMode()
66-
const { isEnabled: isRunStream } = useFeature('runs')
6771
const { playground, hasActiveStream, resetChat, onBack, stopStreaming } =
6872
usePlaygroundLogic({
6973
commit,
@@ -89,11 +93,13 @@ export function DocumentEditorContentArea({
8993
return (
9094
<>
9195
<div className='relative flex-1 flex flex-col h-full min-h-0 overflow-hidden'>
92-
<DocumentEditorHeader
93-
isMerged={isMerged}
94-
isPlaygroundOpen={isPlaygroundOpen}
95-
togglePlaygroundOpen={togglePlaygroundOpen}
96-
/>
96+
{!isEditorSidebarFlagLoading && !isEditorSidebarEnabled && (
97+
<DocumentEditorHeader
98+
isMerged={isMerged}
99+
isPlaygroundOpen={isPlaygroundOpen}
100+
togglePlaygroundOpen={togglePlaygroundOpen}
101+
/>
102+
)}
97103

98104
{/* === SLIDING WRAPPER === */}
99105
<div
@@ -116,17 +122,21 @@ export function DocumentEditorContentArea({
116122
)}
117123
>
118124
<div className='min-h-0 relative z-0 flex flex-col gap-4 h-full'>
119-
<AgentToolbar
120-
isMerged={isMerged}
121-
isAgent={metadata?.config?.type === 'agent'}
122-
config={metadata?.config}
123-
prompt={document.content}
124-
onChangePrompt={updateDocumentContent}
125-
/>
126-
<FreeRunsBanner
127-
isLatitudeProvider={isLatitudeProvider}
128-
freeRunsCount={freeRunsCount}
129-
/>
125+
{!isEditorSidebarFlagLoading && !isEditorSidebarEnabled && (
126+
<>
127+
<AgentToolbar
128+
isMerged={isMerged}
129+
isAgent={metadata?.config?.type === 'agent'}
130+
config={metadata?.config}
131+
prompt={document.content}
132+
onChangePrompt={updateDocumentContent}
133+
/>
134+
<FreeRunsBanner
135+
isLatitudeProvider={isLatitudeProvider}
136+
freeRunsCount={freeRunsCount}
137+
/>
138+
</>
139+
)}
130140
<div
131141
className={cn('flex-1 min-h-0 pb-4', {
132142
'overflow-y-auto custom-scrollbar scrollable-indicator':

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/SidebarArea/Section/index.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type SectionAction = {
2727
iconProps?: IconProps
2828
onClick: () => void
2929
disabled?: boolean
30+
customComponent?: ReactNode
3031
}
3132
const SidebarSection = ({
3233
children,
@@ -44,16 +45,20 @@ const SidebarSection = ({
4445

4546
{actions ? (
4647
<div className='flex flex-row gap-x-2'>
47-
{actions?.map((action, index) => (
48-
<Button
49-
key={index}
50-
variant='ghost'
51-
size='icon'
52-
onClick={action.onClick}
53-
iconProps={action.iconProps ?? { name: 'plus' }}
54-
disabled={action.disabled}
55-
/>
56-
))}
48+
{actions?.map((action, index) =>
49+
action.customComponent ? (
50+
<div key={index}>{action.customComponent}</div>
51+
) : (
52+
<Button
53+
key={index}
54+
variant='ghost'
55+
size='icon'
56+
onClick={action.onClick}
57+
iconProps={action.iconProps ?? { name: 'plus' }}
58+
disabled={action.disabled}
59+
/>
60+
),
61+
)}
5762
</div>
5863
) : null}
5964
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Link from 'next/link'
2+
import { Text } from '@latitude-data/web-ui/atoms/Text'
3+
import { Icon } from '@latitude-data/web-ui/atoms/Icons'
4+
import { Button } from '@latitude-data/web-ui/atoms/Button'
5+
import { ROUTES } from '$/services/routes'
6+
7+
export function SubAgentItem({
8+
agentPath,
9+
documentUuid,
10+
projectId,
11+
commitUuid,
12+
onRemove,
13+
disabled,
14+
}: {
15+
agentPath: string
16+
documentUuid: string
17+
projectId: number
18+
commitUuid: string
19+
onRemove: () => void
20+
disabled: boolean
21+
}) {
22+
const name = agentPath.split('/').pop() || ''
23+
const href = ROUTES.projects
24+
.detail({ id: projectId })
25+
.commits.detail({ uuid: commitUuid })
26+
.documents.detail({ uuid: documentUuid }).root
27+
28+
return (
29+
<div
30+
role='button'
31+
tabIndex={0}
32+
aria-disabled={disabled}
33+
className='flex flex-row items-center rounded-md gap-3 min-w-0 p-2 hover:bg-backgroundCode group'
34+
>
35+
<Link href={href} className='flex items-center gap-3 flex-1 min-w-0'>
36+
<Icon name='bot' color='foregroundMuted' />
37+
<div className='flex-1 min-w-0'>
38+
<Text.H5 ellipsis noWrap>
39+
{name}
40+
</Text.H5>
41+
</div>
42+
</Link>
43+
<Button
44+
disabled={disabled}
45+
variant='ghost'
46+
size='small'
47+
className='opacity-0 group-hover:opacity-100 transition'
48+
iconProps={{ name: 'trash', color: 'foregroundMuted' }}
49+
onClick={(e) => {
50+
e.stopPropagation()
51+
onRemove()
52+
}}
53+
/>
54+
</div>
55+
)
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useMemo, useCallback } from 'react'
2+
import { useCurrentCommit } from '$/app/providers/CommitProvider'
3+
import { useCurrentDocument } from '$/app/providers/DocumentProvider'
4+
import { useCurrentProject } from '$/app/providers/ProjectProvider'
5+
import { useSidebarStore } from '../hooks/useSidebarStore'
6+
import { usePromptConfigInSidebar } from '../hooks/usePromptConfigInSidebar'
7+
import { SidebarSection } from '../Section'
8+
import { SubAgentItem } from './SubAgentItem'
9+
import {
10+
resolveRelativePath,
11+
createRelativePath,
12+
} from '@latitude-data/constants'
13+
import { MultiSelect } from '@latitude-data/web-ui/molecules/MultiSelect'
14+
import { Button } from '@latitude-data/web-ui/atoms/Button'
15+
16+
export function SubAgentsSidebarSection() {
17+
const { commit } = useCurrentCommit()
18+
const { project } = useCurrentProject()
19+
const { document } = useCurrentDocument()
20+
const isLive = !!commit.mergedAt
21+
const { toggleAgent } = usePromptConfigInSidebar()
22+
23+
const { selectedAgents, pathToUuidMap } = useSidebarStore((state) => ({
24+
selectedAgents: state.selectedAgents,
25+
pathToUuidMap: state.pathToUuidMap,
26+
}))
27+
28+
const availableAgents = useMemo(() => {
29+
if (!pathToUuidMap || Object.keys(pathToUuidMap).length === 0) return []
30+
return Object.keys(pathToUuidMap).filter(
31+
(agentPath) => agentPath !== document.path,
32+
)
33+
}, [pathToUuidMap, document.path])
34+
35+
const selectedAgentsFullPaths = useMemo(() => {
36+
if (!Array.isArray(selectedAgents)) return []
37+
return selectedAgents
38+
.filter(Boolean)
39+
.map((relativePath) => resolveRelativePath(relativePath, document.path))
40+
}, [selectedAgents, document.path])
41+
42+
const agentOptions = useMemo(() => {
43+
return availableAgents.map((agentPath) => ({
44+
label: agentPath.split('/').pop() || agentPath,
45+
value: agentPath,
46+
icon: 'bot' as const,
47+
}))
48+
}, [availableAgents])
49+
50+
const handleAgentsChange = useCallback(
51+
(selectedPaths: string[]) => {
52+
// Convert full paths to relative paths
53+
const relativePaths = selectedPaths.map((fullPath) =>
54+
createRelativePath(fullPath, document.path),
55+
)
56+
57+
// Find which agents were added or removed
58+
const currentRelativePaths = selectedAgents
59+
const addedPaths = relativePaths.filter(
60+
(path) => !currentRelativePaths.includes(path),
61+
)
62+
const removedPaths = currentRelativePaths.filter(
63+
(path) => !relativePaths.includes(path),
64+
)
65+
66+
// Toggle each added/removed agent
67+
addedPaths.forEach((path) => {
68+
const fullPath = resolveRelativePath(path, document.path)
69+
toggleAgent(fullPath)
70+
})
71+
removedPaths.forEach((path) => {
72+
const fullPath = resolveRelativePath(path, document.path)
73+
toggleAgent(fullPath)
74+
})
75+
},
76+
[selectedAgents, document.path, toggleAgent],
77+
)
78+
79+
const actions = useMemo(
80+
() => [
81+
{
82+
onClick: () => {},
83+
disabled: isLive,
84+
customComponent: (
85+
<MultiSelect
86+
options={agentOptions}
87+
value={selectedAgentsFullPaths}
88+
onChange={handleAgentsChange}
89+
disabled={isLive}
90+
modalPopover
91+
trigger={
92+
<Button
93+
variant='ghost'
94+
size='small'
95+
iconProps={{ name: 'plus', color: 'foregroundMuted' }}
96+
disabled={isLive}
97+
/>
98+
}
99+
/>
100+
),
101+
},
102+
],
103+
[agentOptions, selectedAgentsFullPaths, handleAgentsChange, isLive],
104+
)
105+
106+
return (
107+
<SidebarSection title='Sub-agents' actions={actions}>
108+
<div className='flex flex-col'>
109+
{selectedAgentsFullPaths.map((agentPath) => {
110+
const documentUuid = pathToUuidMap[agentPath]
111+
return (
112+
<SubAgentItem
113+
key={agentPath}
114+
agentPath={agentPath}
115+
documentUuid={documentUuid}
116+
projectId={project.id}
117+
commitUuid={commit.uuid}
118+
onRemove={() => toggleAgent(agentPath)}
119+
disabled={isLive}
120+
/>
121+
)
122+
})}
123+
</div>
124+
</SidebarSection>
125+
)
126+
}

0 commit comments

Comments
 (0)