Skip to content

Commit 4052ceb

Browse files
committed
feat(workflow): implement custom nodes with management features
- Add functionality to create, rename, and delete custom nodes - Implement custom node operations in WorkflowSidebar - Add UI controls for node management (save, edit, delete buttons) - Add file I/O operations for custom node persistence - Implement message handling between webview and extension - Add custom nodes accordion in WorkflowSidebar - Improve UX with icons and tooltips for workflow actions - Add title truncation for custom node display - Remove handlePostMessage for simplified component architecture - Update rule file paths and search directories
1 parent 4fe511e commit 4052ceb

File tree

12 files changed

+583
-108
lines changed

12 files changed

+583
-108
lines changed

lib/shared/src/rules/rules.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ function isValidPatternFilters(v: any): v is PatternFilters {
119119
export function ruleFileDisplayName(uri: URI, root: URI): string {
120120
return posixFilePaths
121121
.relative(root.path, uri.path)
122-
.replace(/\.sourcegraph\/([^/]+)\.rule\.md$/, '$1')
122+
.replace(/\.sourcegraph\/rules\/([^/]+)\.rule\.md$/, '$1')
123123
}
124124

125125
export function isRuleFilename(file: string | URI): boolean {
@@ -139,7 +139,7 @@ export function ruleSearchPaths(uri: URI, root: URI): URI[] {
139139
break
140140
}
141141
current = current.with({ path: pathFuncs.dirname(current.path) })
142-
searchPaths.push(current.with({ path: pathFuncs.resolve(current.path, '.sourcegraph') }))
142+
searchPaths.push(current.with({ path: pathFuncs.resolve(current.path, '.sourcegraph/rules') }))
143143
}
144144
return searchPaths
145145
}

vscode/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "cody-ai",
44
"private": true,
55
"displayName": "Cody: AI Code Assistant",
6-
"version": "1.69.0+1",
6+
"version": "1.69.0+2",
77
"publisher": "sourcegraph",
88
"license": "Apache-2.0",
99
"icon": "resources/sourcegraph.png",

vscode/src/workflow/workflow-io.ts

+191
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as vscode from 'vscode'
2+
import type { WorkflowNodes } from '../../webviews/workflow/components/nodes/Nodes'
23
import { writeToCodyJSON } from '../commands/utils/config-file'
34
import { migrateWorkflowData } from './workflow-migration'
45

@@ -76,3 +77,193 @@ export async function handleWorkflowLoad(): Promise<any> {
7677
}
7778
return []
7879
}
80+
81+
const CODY_NODES_DIR = '.cody/nodes'
82+
83+
/**
84+
* Retrieves an array of custom workflow nodes from the `.cody/nodes` directory in the current workspace.
85+
*
86+
* This function ensures the `.cody/nodes` directory exists, and then reads all the JSON files in that directory,
87+
* parsing them as `WorkflowNodes` objects and returning them in an array.
88+
*
89+
* If the `.cody/nodes` directory does not exist, or if there are any errors loading the custom nodes, the function
90+
* will return an empty array and log the errors.
91+
*
92+
* @returns An array of `WorkflowNodes` objects representing the custom workflow nodes.
93+
*/
94+
export async function getCustomNodes(): Promise<WorkflowNodes[]> {
95+
try {
96+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri
97+
if (!workspaceRoot) {
98+
vscode.window.showErrorMessage('No workspace found.')
99+
return []
100+
}
101+
const nodesDirUri = vscode.Uri.joinPath(workspaceRoot, CODY_NODES_DIR)
102+
try {
103+
// Ensure the directory exists. If it doesn't, return an empty array.
104+
await vscode.workspace.fs.createDirectory(nodesDirUri)
105+
} catch (e: any) {
106+
if (e.code !== 'FileExists') {
107+
console.warn('Directory .cody/nodes does not exist.')
108+
return []
109+
}
110+
}
111+
112+
const files = await vscode.workspace.fs.readDirectory(nodesDirUri)
113+
const nodes: WorkflowNodes[] = []
114+
115+
for (const [filename, fileType] of files) {
116+
if (fileType === vscode.FileType.File && filename.endsWith('.json')) {
117+
try {
118+
const fileUri = vscode.Uri.joinPath(nodesDirUri, filename)
119+
const fileData = await vscode.workspace.fs.readFile(fileUri)
120+
const node = JSON.parse(fileData.toString()) as WorkflowNodes
121+
nodes.push(node)
122+
} catch (error: any) {
123+
console.error(`Failed to load custom node "${filename}": ${error.message}`)
124+
vscode.window.showErrorMessage(
125+
`Failed to load custom node "${filename}": ${error.message}`
126+
)
127+
}
128+
}
129+
}
130+
131+
return nodes
132+
} catch (error: any) {
133+
console.error(`Failed to load custom nodes: ${error.message}`)
134+
vscode.window.showErrorMessage(`Failed to load custom nodes: ${error.message}`)
135+
return []
136+
}
137+
}
138+
139+
/**
140+
* Saves a custom workflow node to the `.cody/nodes` directory.
141+
*
142+
* If the `.cody/nodes` directory does not exist, it will be created. The node is saved as a JSON file with the
143+
* sanitized title of the node as the filename.
144+
*
145+
* @param node - The `WorkflowNodes` object to be saved.
146+
* @returns A Promise that resolves when the node has been saved successfully.
147+
*/
148+
export async function saveCustomNodes(node: WorkflowNodes): Promise<void> {
149+
try {
150+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri
151+
if (!workspaceRoot) {
152+
vscode.window.showErrorMessage('No workspace found.')
153+
return
154+
}
155+
const nodesDirUri = vscode.Uri.joinPath(workspaceRoot, CODY_NODES_DIR)
156+
try {
157+
await vscode.workspace.fs.createDirectory(nodesDirUri)
158+
} catch (e: any) {
159+
if (e.code !== 'FileExists') {
160+
vscode.window.showErrorMessage(`Failed to create directory: ${e.message}`)
161+
return
162+
}
163+
}
164+
165+
const filename = `${sanitizeFilename(node.data.title)}.json`
166+
const fileUri = vscode.Uri.joinPath(nodesDirUri, filename)
167+
const { id, ...nodeToSave } = node
168+
await writeToCodyJSON(fileUri, nodeToSave)
169+
vscode.window.showInformationMessage(`Custom node "${node.data.title}" saved successfully.`)
170+
} catch (error: any) {
171+
vscode.window.showErrorMessage(`Failed to save custom node: ${error.message}`)
172+
}
173+
}
174+
175+
/**
176+
* Deletes a custom workflow node from the `.cody/nodes` directory.
177+
*
178+
* The function first checks if the workspace has a valid root directory, and then displays a warning message to confirm the deletion of the custom node. If the user confirms the deletion, the function finds the corresponding JSON file in the `.cody/nodes` directory and deletes it. If the file is not found, an error message is displayed.
179+
*
180+
* @param nodeTitle - The title of the custom node to be deleted.
181+
* @returns A Promise that resolves when the node has been deleted successfully.
182+
*/
183+
export async function deleteCustomNode(nodeTitle: string): Promise<void> {
184+
try {
185+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri
186+
if (!workspaceRoot) {
187+
vscode.window.showErrorMessage('No workspace found.')
188+
return
189+
}
190+
const confirmed = await vscode.window.showWarningMessage(
191+
`Delete custom node "${nodeTitle}"?`,
192+
{ modal: true },
193+
'Delete'
194+
)
195+
if (confirmed !== 'Delete') {
196+
return
197+
}
198+
199+
const nodesDirUri = vscode.Uri.joinPath(workspaceRoot, CODY_NODES_DIR)
200+
const files = await vscode.workspace.fs.readDirectory(nodesDirUri)
201+
const nodeFile = files.find(([filename]) => filename.startsWith(sanitizeFilename(nodeTitle)))
202+
if (!nodeFile) {
203+
vscode.window.showErrorMessage(`Custom node with title "${nodeTitle}" not found.`)
204+
return
205+
}
206+
const fileUri = vscode.Uri.joinPath(nodesDirUri, nodeFile[0])
207+
await vscode.workspace.fs.delete(fileUri)
208+
vscode.window.showInformationMessage(
209+
`Custom node with title "${nodeTitle}" deleted successfully.`
210+
)
211+
} catch (error: any) {
212+
vscode.window.showErrorMessage(`Failed to delete custom node: ${error.message}`)
213+
}
214+
}
215+
216+
export async function renameCustomNode(oldNodeTitle: string, newNodeTitle: string): Promise<void> {
217+
try {
218+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri
219+
if (!workspaceRoot) {
220+
vscode.window.showErrorMessage('No workspace found.')
221+
return
222+
}
223+
224+
const nodesDirUri = vscode.Uri.joinPath(workspaceRoot, CODY_NODES_DIR)
225+
const files = await vscode.workspace.fs.readDirectory(nodesDirUri)
226+
const oldNodeFile = files.find(([filename]) =>
227+
filename.startsWith(sanitizeFilename(oldNodeTitle))
228+
)
229+
230+
if (!oldNodeFile) {
231+
vscode.window.showErrorMessage(`Custom node with title "${oldNodeTitle}" not found.`)
232+
return
233+
}
234+
235+
const oldFileUri = vscode.Uri.joinPath(nodesDirUri, oldNodeFile[0])
236+
const fileData = await vscode.workspace.fs.readFile(oldFileUri)
237+
const node = JSON.parse(fileData.toString()) as WorkflowNodes
238+
239+
// Update the node's title
240+
node.data.title = newNodeTitle
241+
242+
// Construct the new file URI
243+
const newFilename = `${sanitizeFilename(newNodeTitle)}.json`
244+
const newFileUri = vscode.Uri.joinPath(nodesDirUri, newFilename)
245+
246+
// Write the updated node to the new file
247+
const { id, ...nodeToSave } = node
248+
await writeToCodyJSON(newFileUri, nodeToSave)
249+
250+
// Delete the old file
251+
await vscode.workspace.fs.delete(oldFileUri)
252+
253+
vscode.window.showInformationMessage(
254+
`Custom node "${oldNodeTitle}" renamed to "${newNodeTitle}" successfully.`
255+
)
256+
} catch (error: any) {
257+
vscode.window.showErrorMessage(`Failed to rename custom node: ${error.message}`)
258+
}
259+
}
260+
261+
/**
262+
* Sanitizes a filename by replacing any non-alphanumeric, non-underscore, and non-hyphen characters with an underscore.
263+
*
264+
* @param name - The filename to be sanitized.
265+
* @returns The sanitized filename.
266+
*/
267+
function sanitizeFilename(name: string): string {
268+
return name.replace(/[^a-zA-Z0-9_-]/g, '_')
269+
}

vscode/src/workflow/workflow.ts

+47-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ import {
1313
} from '@sourcegraph/cody-shared'
1414
import type { ContextRetriever } from '../chat/chat-view/ContextRetriever'
1515
import { executeWorkflow } from './workflow-executor'
16-
import { handleWorkflowLoad, handleWorkflowSave } from './workflow-io'
16+
import {
17+
deleteCustomNode,
18+
getCustomNodes,
19+
handleWorkflowLoad,
20+
handleWorkflowSave,
21+
renameCustomNode,
22+
saveCustomNodes,
23+
} from './workflow-io'
1724

1825
/**
1926
* Registers the Cody workflow commands in the Visual Studio Code extension context.
@@ -58,9 +65,9 @@ export function registerWorkflowCommands(
5865
case 'get_models': {
5966
const chatModelsSubscription = modelsService
6067
.getModels(ModelUsage.Chat)
61-
.subscribe(models => {
68+
.subscribe(async models => {
6269
if (models !== pendingOperation) {
63-
panel.webview.postMessage({
70+
await panel.webview.postMessage({
6471
type: 'models_loaded',
6572
data: models,
6673
} as ExtensionToWorkflow)
@@ -75,7 +82,7 @@ export function registerWorkflowCommands(
7582
case 'load_workflow': {
7683
const loadedData = await handleWorkflowLoad()
7784
if (loadedData) {
78-
panel.webview.postMessage({
85+
await panel.webview.postMessage({
7986
type: 'workflow_loaded',
8087
data: loadedData,
8188
} as ExtensionToWorkflow)
@@ -109,7 +116,7 @@ export function registerWorkflowCommands(
109116
break
110117
case 'calculate_tokens': {
111118
const count = await TokenCounterUtils.encode(message.data.text)
112-
panel.webview.postMessage({
119+
await panel.webview.postMessage({
113120
type: 'token_count',
114121
data: {
115122
nodeId: message.data.nodeId,
@@ -132,6 +139,41 @@ export function registerWorkflowCommands(
132139
vscode.env.openExternal(url)
133140
break
134141
}
142+
case 'save_customNode': {
143+
await saveCustomNodes(message.data)
144+
const nodes = await getCustomNodes()
145+
await panel.webview.postMessage({
146+
type: 'provide_custom_nodes',
147+
data: nodes,
148+
} as ExtensionToWorkflow)
149+
break
150+
}
151+
case 'get_custom_nodes': {
152+
const nodes = await getCustomNodes()
153+
await panel.webview.postMessage({
154+
type: 'provide_custom_nodes',
155+
data: nodes,
156+
} as ExtensionToWorkflow)
157+
break
158+
}
159+
case 'delete_customNode': {
160+
await deleteCustomNode(message.data)
161+
const nodes = await getCustomNodes()
162+
await panel.webview.postMessage({
163+
type: 'provide_custom_nodes',
164+
data: nodes,
165+
} as ExtensionToWorkflow)
166+
break
167+
}
168+
case 'rename_customNode': {
169+
await renameCustomNode(message.data.oldNodeTitle, message.data.newNodeTitle)
170+
const nodes = await getCustomNodes()
171+
await panel.webview.postMessage({
172+
type: 'provide_custom_nodes',
173+
data: nodes,
174+
} as ExtensionToWorkflow)
175+
break
176+
}
135177
}
136178
},
137179
undefined,

0 commit comments

Comments
 (0)