Skip to content

Commit ae2fdc7

Browse files
fix: detect changes in the expert when the user manually updates through the terminal or file explorer. (#87)
1 parent 39cc87d commit ae2fdc7

File tree

10 files changed

+181
-82
lines changed

10 files changed

+181
-82
lines changed

.changeset/healthy-houses-sin.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"hai-build-code-generator": patch
3+
---
4+
5+
Automatically sync custom experts when users manually update or delete experts.
6+
Replaced home icon with plus icon and adjusted the order.
7+
Added validation to ensure the file exists before deleting it from the context directory.
8+
Added schema validation for expert data.

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
{
8585
"command": "hai.plusButtonClicked",
8686
"title": "New Task",
87-
"icon": "$(home)"
87+
"icon": "$(add)"
8888
},
8989
{
9090
"command": "hai.mcpButtonClicked",
@@ -147,17 +147,17 @@
147147
"menus": {
148148
"view/title": [
149149
{
150-
"command": "hai.haiBuildTaskListClicked",
150+
"command": "hai.plusButtonClicked",
151151
"group": "navigation@1",
152152
"when": "view == hai.SidebarProvider"
153153
},
154154
{
155-
"command": "hai.expertsButtonClicked",
156-
"group": "navigation@2",
155+
"command": "hai.haiBuildTaskListClicked",
156+
"group": "navigation@1",
157157
"when": "view == hai.SidebarProvider"
158158
},
159159
{
160-
"command": "hai.plusButtonClicked",
160+
"command": "hai.expertsButtonClicked",
161161
"group": "navigation@2",
162162
"when": "view == hai.SidebarProvider"
163163
},

src/core/controller/index.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ export class Controller {
734734
}
735735

736736
// plan act setting
737-
if (message.planActSeparateModelsSetting != undefined) {
737+
if (message.planActSeparateModelsSetting !== undefined) {
738738
await customUpdateState(this.context, "planActSeparateModelsSetting", message.planActSeparateModelsSetting)
739739
}
740740

@@ -835,11 +835,7 @@ export class Controller {
835835
}
836836
break
837837
case "loadExperts":
838-
const experts = await this.expertManager.readExperts(this.vsCodeWorkSpaceFolderFsPath)
839-
await this.postMessageToWebview({
840-
type: "expertsUpdated",
841-
experts,
842-
})
838+
await this.loadExperts()
843839
break
844840
case "onHaiConfigure":
845841
const isConfigureEnabled = message.bool !== undefined ? message.bool : true
@@ -2503,4 +2499,12 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
25032499
})
25042500
await this.postStateToWebview()
25052501
}
2502+
2503+
async loadExperts() {
2504+
const experts = await this.expertManager.readExperts(this.vsCodeWorkSpaceFolderFsPath)
2505+
await this.postMessageToWebview({
2506+
type: "expertsUpdated",
2507+
experts,
2508+
})
2509+
}
25062510
}

src/core/experts/ExpertManager.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import fs from "fs/promises"
22
import * as path from "path"
3-
import { ExpertData } from "../../../webview-ui/src/types/experts"
3+
import * as vscode from "vscode"
4+
import { ExpertData, ExpertDataSchema } from "../../../webview-ui/src/types/experts"
45
import { fileExistsAtPath, createDirectoriesForFile } from "../../utils/fs"
6+
import { GlobalFileNames } from "../../global-constants"
57

68
export class ExpertManager {
79
/**
@@ -14,11 +16,17 @@ export class ExpertManager {
1416
throw new Error("No workspace path provided")
1517
}
1618

19+
// Validate expert data with Zod schema
20+
const validationResult = ExpertDataSchema.safeParse(expert)
21+
if (!validationResult.success) {
22+
throw new Error(`Invalid expert data: ${validationResult.error.message}`)
23+
}
24+
1725
// Create a sanitized folder name from the expert name
1826
const sanitizedName = expert.name.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase()
1927

2028
// Create the expert directory
21-
const expertDir = path.join(workspacePath, ".hai-experts", sanitizedName)
29+
const expertDir = path.join(workspacePath, GlobalFileNames.experts, sanitizedName)
2230
await createDirectoriesForFile(path.join(expertDir, "placeholder.txt"))
2331

2432
// Create metadata file
@@ -45,7 +53,7 @@ export class ExpertManager {
4553
return []
4654
}
4755

48-
const expertsDir = path.join(workspacePath, ".hai-experts")
56+
const expertsDir = path.join(workspacePath, GlobalFileNames.experts)
4957
if (!(await fileExistsAtPath(expertsDir))) {
5058
return []
5159
}
@@ -62,22 +70,31 @@ export class ExpertManager {
6270
try {
6371
// Read metadata
6472
const metadataPath = path.join(expertDir, "metadata.json")
65-
if (await fileExistsAtPath(metadataPath)) {
73+
const promptPath = path.join(expertDir, "prompt.md")
74+
75+
if ((await fileExistsAtPath(metadataPath)) && (await fileExistsAtPath(promptPath))) {
6676
const metadataContent = await fs.readFile(metadataPath, "utf-8")
6777
const metadata = JSON.parse(metadataContent)
6878

6979
// Read prompt
70-
const promptPath = path.join(expertDir, "prompt.md")
71-
const promptContent = (await fileExistsAtPath(promptPath))
72-
? await fs.readFile(promptPath, "utf-8")
73-
: ""
80+
const promptContent = await fs.readFile(promptPath, "utf-8")
7481

75-
experts.push({
82+
const expertData = {
7683
name: metadata.name,
7784
isDefault: metadata.isDefault,
7885
prompt: promptContent,
7986
createdAt: metadata.createdAt,
80-
})
87+
}
88+
89+
// Validate expert data with Zod schema
90+
const validationResult = ExpertDataSchema.safeParse(expertData)
91+
if (validationResult.success) {
92+
experts.push(expertData)
93+
} else {
94+
vscode.window.showWarningMessage(
95+
`Invalid expert data for ${folder}: ${validationResult.error.issues.map((issue) => issue.message).join(", ")}`,
96+
)
97+
}
8198
}
8299
} catch (error) {
83100
console.error(`Failed to read expert from ${folder}:`, error)
@@ -102,7 +119,12 @@ export class ExpertManager {
102119
throw new Error("No workspace path provided")
103120
}
104121

105-
const expertsDir = path.join(workspacePath, ".hai-experts")
122+
// Validate expert name
123+
if (!expertName || typeof expertName !== "string") {
124+
throw new Error("Expert name must be a non-empty string")
125+
}
126+
127+
const expertsDir = path.join(workspacePath, GlobalFileNames.experts)
106128
if (!(await fileExistsAtPath(expertsDir))) {
107129
return
108130
}
@@ -145,7 +167,12 @@ export class ExpertManager {
145167
throw new Error("No workspace path provided")
146168
}
147169

148-
const expertsDir = path.join(workspacePath, ".hai-experts")
170+
// Validate expert name
171+
if (!expertName || typeof expertName !== "string") {
172+
throw new Error("Expert name must be a non-empty string")
173+
}
174+
175+
const expertsDir = path.join(workspacePath, GlobalFileNames.experts)
149176
if (!(await fileExistsAtPath(expertsDir))) {
150177
return null
151178
}

src/global-constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export const GlobalFileNames = {
55
openRouterModels: "openrouter_models.json",
66
mcpSettings: "hai_mcp_settings.json",
77
clineRules: ".hairules",
8+
experts: ".hai-experts",
89
}

src/integrations/workspace/HaiFileSystemWatcher.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ class HaiFileSystemWatcher {
7272
this.providerRef.deref()?.updateHaiRulesState(true)
7373
}
7474

75+
// Check for the experts
76+
if (filePath.includes(GlobalFileNames.experts)) {
77+
this.providerRef.deref()?.loadExperts()
78+
}
79+
7580
this.providerRef.deref()?.invokeReindex([filePath], FileOperations.Delete)
7681
})
7782

@@ -83,11 +88,20 @@ class HaiFileSystemWatcher {
8388
this.providerRef.deref()?.updateHaiRulesState(true)
8489
}
8590

91+
// Check for the experts
92+
if (filePath.includes(GlobalFileNames.experts)) {
93+
this.providerRef.deref()?.loadExperts()
94+
}
95+
8696
this.providerRef.deref()?.invokeReindex([filePath], FileOperations.Create)
8797
})
8898

8999
this.watcher.on("change", (filePath) => {
90100
console.log("HaiFileSystemWatcher File changes", filePath)
101+
// Check for the experts
102+
if (filePath.includes(GlobalFileNames.experts)) {
103+
this.providerRef.deref()?.loadExperts()
104+
}
91105
this.providerRef.deref()?.invokeReindex([filePath], FileOperations.Change)
92106
})
93107
}

src/utils/delete-helper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export async function deleteFromContextDirectory(filePaths: string[], srcFolder:
1010
// Delete files from context directory
1111
for (const filePath of filePaths) {
1212
const destinationFilePath = filePath.replace(srcFolder, destinationFolderPath)
13-
rmSync(destinationFilePath, { recursive: true })
13+
if (existsSync(destinationFilePath)) {
14+
rmSync(destinationFilePath, { recursive: true })
15+
}
1416
}
1517

1618
// Handle hash file updates

webview-ui/src/components/experts/ExpertsView.tsx

Lines changed: 78 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { VSCodeButton, VSCodeTextField, VSCodeTextArea } from "@vscode/webview-u
44
import { vscode } from "../../utils/vscode"
55
import { DEFAULT_EXPERTS } from "../../data/defaultExperts"
66
import { ExpertData } from "../../types/experts"
7+
import { useExtensionState } from "../../context/ExtensionStateContext"
78

89
interface ExpertsViewProps {
910
onDone: () => void
@@ -18,6 +19,7 @@ const ExpertsView: React.FC<ExpertsViewProps> = ({ onDone }) => {
1819
const [isFormReadOnly, setIsFormReadOnly] = useState(false)
1920
const [nameError, setNameError] = useState<string | null>(null)
2021
const [expertInDeleteConfirmation, setExpertInDeleteConfirmation] = useState<string | null>(null)
22+
const { vscodeWorkspacePath } = useExtensionState()
2123

2224
// Create a reference to the file input element
2325
const fileInputRef = React.useRef<HTMLInputElement>(null)
@@ -363,64 +365,84 @@ const ExpertsView: React.FC<ExpertsViewProps> = ({ onDone }) => {
363365
</Section>
364366

365367
{/* Add/Edit Form Section */}
366-
<Section>
367-
<SectionHeader>Add New Expert</SectionHeader>
368-
<FormContainer>
369-
<FormGroup>
370-
<label htmlFor="expert-name">Name</label>
371-
<VSCodeTextField
372-
id="expert-name"
373-
value={newExpertName}
374-
onChange={(e) => setNewExpertName((e.target as HTMLInputElement).value)}
375-
placeholder="Expert Name"
376-
style={{ width: "100%" }}
377-
disabled={isFormReadOnly}
378-
/>
379-
{nameError && (
380-
<p style={{ color: "var(--vscode-errorForeground)", fontSize: "12px", marginTop: "4px" }}>
381-
{nameError}
382-
</p>
383-
)}
384-
</FormGroup>
385-
386-
<FormGroup>
387-
<label htmlFor="expert-prompt">GuideLines</label>
388-
<VSCodeTextArea
389-
id="expert-prompt"
390-
value={newExpertPrompt}
391-
onChange={(e) => setNewExpertPrompt((e.target as HTMLTextAreaElement).value)}
392-
placeholder="Enter Expert Guidelines"
393-
resize="vertical"
394-
rows={6}
395-
disabled={isFormReadOnly}
396-
style={{ width: "100%" }}
397-
/>
398-
<p className="description-text">
399-
These guidelines will override the default HAI guidelines when this expert is selected.
400-
</p>
401-
</FormGroup>
402-
403-
{!isFormReadOnly && (
368+
369+
{vscodeWorkspacePath ? (
370+
<Section>
371+
<SectionHeader>Add New Expert</SectionHeader>
372+
<FormContainer>
404373
<FormGroup>
405-
<VSCodeButton appearance="secondary" onClick={handleFileUpload}>
406-
<span className="codicon codicon-cloud-upload" style={{ marginRight: "5px" }}></span>
407-
Upload Guidelines File (.md only)
408-
</VSCodeButton>
374+
<label htmlFor="expert-name">Name</label>
375+
<VSCodeTextField
376+
id="expert-name"
377+
value={newExpertName}
378+
onChange={(e) => setNewExpertName((e.target as HTMLInputElement).value)}
379+
placeholder="Expert Name"
380+
style={{ width: "100%" }}
381+
disabled={isFormReadOnly}
382+
/>
383+
{nameError && (
384+
<p style={{ color: "var(--vscode-errorForeground)", fontSize: "12px", marginTop: "4px" }}>
385+
{nameError}
386+
</p>
387+
)}
409388
</FormGroup>
410-
)}
411-
412-
{!isFormReadOnly && (
413-
<ActionButtons>
414-
<VSCodeButton appearance="secondary" onClick={resetForm}>
415-
Cancel
416-
</VSCodeButton>
417-
<VSCodeButton appearance="primary" onClick={handleSaveExpert}>
418-
Save
419-
</VSCodeButton>
420-
</ActionButtons>
421-
)}
422-
</FormContainer>
423-
</Section>
389+
390+
<FormGroup>
391+
<label htmlFor="expert-prompt">Guidelines</label>
392+
<VSCodeTextArea
393+
id="expert-prompt"
394+
value={newExpertPrompt}
395+
onChange={(e) => setNewExpertPrompt((e.target as HTMLTextAreaElement).value)}
396+
placeholder="Enter Expert Guidelines"
397+
resize="vertical"
398+
rows={6}
399+
disabled={isFormReadOnly}
400+
style={{ width: "100%" }}
401+
/>
402+
<p className="description-text">
403+
These guidelines will override the default HAI guidelines when this expert is selected.
404+
</p>
405+
</FormGroup>
406+
407+
{!isFormReadOnly && (
408+
<FormGroup>
409+
<VSCodeButton appearance="secondary" onClick={handleFileUpload}>
410+
<span className="codicon codicon-cloud-upload" style={{ marginRight: "5px" }}></span>
411+
Upload Guidelines File (.md only)
412+
</VSCodeButton>
413+
</FormGroup>
414+
)}
415+
416+
{!isFormReadOnly && (
417+
<ActionButtons>
418+
<VSCodeButton appearance="secondary" onClick={resetForm}>
419+
Cancel
420+
</VSCodeButton>
421+
<VSCodeButton appearance="primary" onClick={handleSaveExpert}>
422+
Save
423+
</VSCodeButton>
424+
</ActionButtons>
425+
)}
426+
</FormContainer>
427+
</Section>
428+
) : (
429+
<Section>
430+
<SectionHeader>Add New Expert</SectionHeader>
431+
<EmptyState>
432+
<div
433+
style={{
434+
display: "flex",
435+
alignItems: "center",
436+
gap: "8px",
437+
color: "var(--vscode-editorWarning-foreground)",
438+
fontSize: "11px",
439+
}}>
440+
<i className="codicon codicon-warning" />
441+
<span>Workspace is not available. Please open a workspace to add new experts.</span>
442+
</div>
443+
</EmptyState>
444+
</Section>
445+
)}
424446
</Content>
425447
</Container>
426448
)

0 commit comments

Comments
 (0)