Skip to content

Commit eec719b

Browse files
Olasunkanmi OyinlolaOlasunkanmi Oyinlola
authored andcommitted
feat(ui): Enhance CodeBuddy description and add configuration handling
- Update the extension description in package.json to highlight AI-powered features and supported models. - Implement SecretStorageService to securely store API keys and handle configuration changes. - Add configuration change handling to update configurations like font and chatview theme. - Add workspace context to disable input on load. - Publish configuration changes via the event emitter. - Subscribe to configuration changes in the webview to apply new settings.
1 parent b2843f7 commit eec719b

File tree

11 files changed

+118
-65
lines changed

11 files changed

+118
-65
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ config.ts
99
database
1010
files
1111
webviewUi/dist
12-
samples
12+
samples
13+
patterns

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://olasunkanmi.app"
88
},
99
"publisher": "fiatinnovations",
10-
"description": "CodeBuddy is an AI-powered coding assistant that helps developers write, review, and optimize code.",
10+
"description": "CodeBuddy is a Visual Studio Code extension that enhances developer productivity through AI-powered code assistance. It provides intelligent code review, refactoring suggestions, optimization tips, and interactive chat capabilities powered by multiple AI models including Gemini, Groq, Anthropic, and Deepseek.",
1111
"version": "1.2.3",
1212
"engines": {
1313
"vscode": "^1.78.0"

src/application/constant.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ export const APP_CONFIG = {
4747
grokModel: "grok.model",
4848
deepseekApiKey: "deepseek.apiKey",
4949
deepseekModel: "deepseek.model",
50+
font: "font.family",
51+
chatview: "chatview.theme",
52+
chatviewFont: "chatview.font.size",
5053
};
54+
5155
export enum generativeAiModels {
5256
GEMINI = "Gemini",
5357
GROQ = "Groq",

src/emitter/interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ type AgentEventKeys =
1414
| "onStrategizing"
1515
| "onModelChange"
1616
| "onModelChangeSuccess"
17-
| "onHistoryUpdated";
17+
| "onHistoryUpdated"
18+
| "onConfigurationChange";
1819

1920
export type IAgentEventMap = Record<AgentEventKeys, IEventPayload>;
2021

src/emitter/publisher.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export class EventEmitter extends BaseEmitter<Record<string, IEventPayload>> {
3030
);
3131
onHistoryUpdated: vscode.Event<IEventPayload> =
3232
this.createEvent("onHistoryUpdated");
33+
onConfigurationChange: vscode.Event<IEventPayload> = this.createEvent(
34+
"onConfigurationChange",
35+
);
3336

3437
/**
3538
* Emits a generic event with specified status, message, and optional data.

src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { DeepseekWebViewProvider } from "./webview-providers/deepseek";
3232
import { GeminiWebViewProvider } from "./webview-providers/gemini";
3333
import { GroqWebViewProvider } from "./webview-providers/groq";
3434
import { WebViewProviderManager } from "./webview-providers/manager";
35+
import { SecretStorageService } from "./services/secret-storage";
3536

3637
const {
3738
geminiKey,
@@ -84,6 +85,7 @@ async function createFileDB(context: vscode.ExtensionContext) {
8485

8586
export async function activate(context: vscode.ExtensionContext) {
8687
try {
88+
const secretStorageService = new SecretStorageService(context);
8789
await createFileDB(context);
8890
await connectToDatabase(context);
8991
const credentials = new Credentials();
@@ -240,6 +242,7 @@ export async function activate(context: vscode.ExtensionContext) {
240242
...subscriptions,
241243
quickFixCodeAction,
242244
agentEventEmmitter,
245+
secretStorageService,
243246
);
244247
} catch (error) {
245248
Memory.clear();

src/infrastructure/storage/local-storage.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/services/secret-storage.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as vscode from "vscode";
2+
import { Logger, LogLevel } from "../infrastructure/logger/logger";
3+
import { APP_CONFIG } from "../application/constant";
4+
import { Orchestrator } from "../agents/orchestrator";
5+
6+
export class SecretStorageService implements vscode.Disposable {
7+
private readonly localStorage: vscode.SecretStorage;
8+
private readonly logger: Logger;
9+
private readonly disposables: vscode.Disposable[] = [];
10+
protected readonly orchestrator: Orchestrator;
11+
12+
constructor(private readonly context: vscode.ExtensionContext) {
13+
this.localStorage = context.secrets;
14+
this.logger = Logger.initialize("LocalStorageManager", {
15+
minLevel: LogLevel.DEBUG,
16+
});
17+
this.orchestrator = Orchestrator.getInstance();
18+
this.disposables.push(
19+
this.localStorage.onDidChange(this.handleSecretStorageChange.bind(this)),
20+
);
21+
this.disposables.push(
22+
vscode.workspace.onDidChangeConfiguration(
23+
this.handleConfigurationChange.bind(this),
24+
),
25+
);
26+
}
27+
28+
async add(key: string, value: string): Promise<void> {
29+
await this.localStorage.store(key, value);
30+
}
31+
32+
async get(key: string): Promise<string | undefined> {
33+
return await this.localStorage.get(key);
34+
}
35+
36+
async delete(key: string) {
37+
await this.localStorage.delete(key);
38+
}
39+
40+
async handleSecretStorageChange(event: vscode.SecretStorageChangeEvent) {
41+
const value = await this.localStorage.get(event.key);
42+
this.logger.info(`${event.key}'s value has been changed in SecretStorage`);
43+
return value;
44+
}
45+
46+
private logConfigurationChange(configKey: string, messagePrefix: string) {
47+
const data: any = {};
48+
const newValue = vscode.workspace
49+
.getConfiguration()
50+
.get<string | number>(configKey);
51+
const sensitiveValues = newValue ? "Configured" : "Not Configured";
52+
const logMessage =
53+
typeof newValue === "string" &&
54+
(configKey.endsWith("apiKey") || configKey.endsWith("apiKeys"))
55+
? `${messagePrefix} ${sensitiveValues}`
56+
: `${messagePrefix} ${newValue}`;
57+
data[configKey] = newValue;
58+
this.logger.info(`Configuration change detected: ${logMessage}`);
59+
this.orchestrator.publish("onConfigurationChange", JSON.stringify(data));
60+
}
61+
62+
async handleConfigurationChange(event: vscode.ConfigurationChangeEvent) {
63+
for (const change of Object.values(APP_CONFIG)) {
64+
if (event.affectsConfiguration(change)) {
65+
this.logConfigurationChange(change, "");
66+
}
67+
}
68+
}
69+
70+
getModelOption(): string | undefined {
71+
return vscode.workspace.getConfiguration().get<string>("option");
72+
}
73+
74+
setModelOption(value: string): Thenable<void> {
75+
return vscode.workspace
76+
.getConfiguration()
77+
.update("generativeAi.option", value, vscode.ConfigurationTarget.Global);
78+
}
79+
80+
public dispose(): void {
81+
this.disposables.forEach((d) => d.dispose());
82+
}
83+
}

src/webview-providers/base.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export abstract class BaseWebViewProvider implements vscode.Disposable {
6565
this.orchestrator.onStrategizing(
6666
this.handleModelResponseEvent.bind(this),
6767
),
68+
this.orchestrator.onConfigurationChange(
69+
this.handleGenericEvents.bind(this),
70+
),
6871
);
6972
}
7073

webviewUi/src/components/context.tsx

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ const WorkspaceSelector: React.FC<WorkspceSelectorProps> = ({
8888
}
8989
}
9090
const [isOpen, setIsOpen] = useState(false);
91-
const [currentCategories, setCurrentCategories] = useState<
92-
WorkspceCategory[]
93-
>([{ id: "activeEditor", name: activeEditor }]);
91+
const [currentCategories, setCurrentCategories] = useState<WorkspceCategory[]>([
92+
{ id: "activeEditor", name: activeEditor },
93+
]);
9494
const [breadcrumbs, setBreadcrumbs] = useState<WorkspceCategory[]>([]);
9595
const inputRef = useRef<HTMLInputElement>(null);
9696
const [inputValue, setInputValue] = useState(initialValue);
@@ -107,23 +107,16 @@ const WorkspaceSelector: React.FC<WorkspceSelectorProps> = ({
107107

108108
if (newValue.length > 0 && newValue.startsWith("@")) {
109109
const lastAtIndex = newValue.lastIndexOf("@");
110-
const lastAtIsValidStart =
111-
lastAtIndex === 0 || newValue[lastAtIndex - 1] === " ";
110+
const lastAtIsValidStart = lastAtIndex === 0 || newValue[lastAtIndex - 1] === " ";
112111
setIsOpen(lastAtIsValidStart);
113112
if (!lastAtIsValidStart) {
114113
setIsOpen(false);
115-
setCurrentCategories([
116-
{ id: "activeEditor", name: activeEditor },
117-
...workspaceFolders,
118-
]);
114+
setCurrentCategories([{ id: "activeEditor", name: activeEditor }, ...workspaceFolders]);
119115
setBreadcrumbs([]);
120116
}
121117
} else {
122118
setIsOpen(false);
123-
setCurrentCategories([
124-
{ id: "activeEditor", name: activeEditor },
125-
...workspaceFolders,
126-
]);
119+
setCurrentCategories([{ id: "activeEditor", name: activeEditor }, ...workspaceFolders]);
127120
setBreadcrumbs([]);
128121
}
129122
};
@@ -145,8 +138,7 @@ const WorkspaceSelector: React.FC<WorkspceSelectorProps> = ({
145138
const parentCategories =
146139
newBreadcrumbs.length === 0
147140
? workspaceFolders
148-
: newBreadcrumbs[newBreadcrumbs.length - 1].children ||
149-
workspaceFolders;
141+
: newBreadcrumbs[newBreadcrumbs.length - 1].children || workspaceFolders;
150142

151143
setCurrentCategories(parentCategories);
152144
setBreadcrumbs(newBreadcrumbs);
@@ -161,24 +153,16 @@ const WorkspaceSelector: React.FC<WorkspceSelectorProps> = ({
161153
const textBeforeCursor = inputValue.substring(0, cursorPosition);
162154
const lastAtIndex = textBeforeCursor.lastIndexOf("@");
163155

164-
if (
165-
lastAtIndex !== -1 &&
166-
(lastAtIndex === 0 || textBeforeCursor[lastAtIndex - 1] === " ")
167-
) {
156+
if (lastAtIndex !== -1 && (lastAtIndex === 0 || textBeforeCursor[lastAtIndex - 1] === " ")) {
168157
const newInputValue =
169-
textBeforeCursor.substring(0, lastAtIndex) +
170-
`@${item.name} ` +
171-
inputValue.substring(cursorPosition);
158+
textBeforeCursor.substring(0, lastAtIndex) + `@${item.name} ` + inputValue.substring(cursorPosition);
172159
inputRef.current.value = newInputValue;
173160
setInputValue(newInputValue);
174161
onInputChange(newInputValue);
175162
inputRef.current.focus();
176163

177164
const newCursorPosition = lastAtIndex + item.name.length + 2;
178-
inputRef.current.setSelectionRange(
179-
newCursorPosition,
180-
newCursorPosition
181-
);
165+
inputRef.current.setSelectionRange(newCursorPosition, newCursorPosition);
182166
} else {
183167
inputRef.current.value = `@${item.name}`;
184168
setInputValue(`@${item.name}`);
@@ -212,6 +196,7 @@ const WorkspaceSelector: React.FC<WorkspceSelectorProps> = ({
212196
style={styles.input}
213197
onChange={handleInputChange}
214198
value={inputValue}
199+
disabled={initialValue.length > 1}
215200
/>
216201

217202
{isOpen && (
@@ -241,11 +226,7 @@ const WorkspaceSelector: React.FC<WorkspceSelectorProps> = ({
241226

242227
<ul style={styles.list}>
243228
{currentCategories.map((category) => (
244-
<li
245-
key={category.id}
246-
className="list-item"
247-
style={styles.listItem}
248-
>
229+
<li key={category.id} className="list-item" style={styles.listItem}>
249230
<button
250231
onClick={() => navigateToCategory(category)}
251232
style={{

0 commit comments

Comments
 (0)