mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Add slack webhook functionality
This commit is contained in:
parent
0100d6cff5
commit
cda6773132
9 changed files with 646 additions and 153 deletions
|
|
@ -51,6 +51,7 @@ import { detectCodeOmission } from "../integrations/editor/detect-omission"
|
|||
import { BrowserSession } from "../services/browser/BrowserSession"
|
||||
import { OpenRouterHandler } from "../api/providers/openrouter"
|
||||
import { McpHub } from "../services/mcp/McpHub"
|
||||
import { SlackNotifier } from "../services/slack"
|
||||
|
||||
const cwd =
|
||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
|
|
@ -66,6 +67,7 @@ export class Cline {
|
|||
private terminalManager: TerminalManager
|
||||
private urlContentFetcher: UrlContentFetcher
|
||||
private browserSession: BrowserSession
|
||||
private slackNotifier?: SlackNotifier
|
||||
private didEditFile: boolean = false
|
||||
customInstructions?: string
|
||||
diffStrategy?: DiffStrategy
|
||||
|
|
@ -96,6 +98,28 @@ export class Cline {
|
|||
private didAlreadyUseTool = false
|
||||
private didCompleteReadingStream = false
|
||||
|
||||
private async notifySlack(type: 'complete' | 'input' | 'fail', message: string) {
|
||||
if (!this.slackNotifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case 'complete':
|
||||
await this.slackNotifier.notifyTaskComplete(message);
|
||||
break;
|
||||
case 'input':
|
||||
await this.slackNotifier.notifyUserInputNeeded(message);
|
||||
break;
|
||||
case 'fail':
|
||||
await this.slackNotifier.notifyTaskFailed(message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Failed to send Slack notification: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
provider: ClineProvider,
|
||||
apiConfiguration: ApiConfiguration,
|
||||
|
|
@ -105,12 +129,40 @@ export class Cline {
|
|||
task?: string | undefined,
|
||||
images?: string[] | undefined,
|
||||
historyItem?: HistoryItem | undefined,
|
||||
slackConfig?: { enabled: boolean; webhookUrl: string }
|
||||
) {
|
||||
// Set taskId first
|
||||
if (historyItem) {
|
||||
this.taskId = historyItem.id
|
||||
} else if (task || images) {
|
||||
this.taskId = Date.now().toString()
|
||||
} else {
|
||||
throw new Error("Either historyItem or task/images must be provided")
|
||||
}
|
||||
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.api = buildApiHandler(apiConfiguration)
|
||||
this.terminalManager = new TerminalManager()
|
||||
this.urlContentFetcher = new UrlContentFetcher(provider.context)
|
||||
this.browserSession = new BrowserSession(provider.context)
|
||||
|
||||
// Initialize Slack notifier
|
||||
if (slackConfig?.enabled && slackConfig?.webhookUrl) {
|
||||
try {
|
||||
this.slackNotifier = new SlackNotifier(slackConfig);
|
||||
|
||||
// Send initialization notification
|
||||
const initMessage = task
|
||||
? `🚀 New task started: ${task}`
|
||||
: historyItem
|
||||
? `📝 Resuming task: ${historyItem.task}`
|
||||
: "🔄 Roo Cline initialized";
|
||||
|
||||
this.notifySlack('input', initMessage).catch(error => {
|
||||
});
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||
this.customInstructions = customInstructions
|
||||
this.diffEnabled = enableDiff ?? false
|
||||
|
|
@ -118,13 +170,9 @@ export class Cline {
|
|||
this.diffStrategy = getDiffStrategy(this.api.getModel().id, fuzzyMatchThreshold ?? 1.0)
|
||||
}
|
||||
if (historyItem) {
|
||||
this.taskId = historyItem.id
|
||||
this.resumeTaskFromHistory()
|
||||
} else if (task || images) {
|
||||
this.taskId = Date.now().toString()
|
||||
this.startTask(task, images)
|
||||
} else {
|
||||
throw new Error("Either historyItem or task/images must be provided")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +214,6 @@ export class Cline {
|
|||
await fs.writeFile(filePath, JSON.stringify(this.apiConversationHistory))
|
||||
} catch (error) {
|
||||
// in the off chance this fails, we don't want to stop the task
|
||||
console.error("Failed to save API conversation history:", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +268,6 @@ export class Cline {
|
|||
totalCost: apiMetrics.totalCost,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to save cline messages:", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +279,11 @@ export class Cline {
|
|||
text?: string,
|
||||
partial?: boolean,
|
||||
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
|
||||
// Send notification for user input needed
|
||||
if (type === "followup" && text && !partial) {
|
||||
await this.notifySlack('input', text)
|
||||
}
|
||||
|
||||
// If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of Cline now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
|
||||
if (this.abort) {
|
||||
throw new Error("Cline instance aborted")
|
||||
|
|
@ -313,10 +364,13 @@ export class Cline {
|
|||
}
|
||||
|
||||
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
|
||||
|
||||
if (this.lastMessageTs !== askTs) {
|
||||
throw new Error("Current ask promise was ignored") // could happen if we send multiple asks in a row i.e. with command_output. It's important that when we know an ask could fail, it is handled gracefully
|
||||
}
|
||||
|
||||
const result = { response: this.askResponse!, text: this.askResponseText, images: this.askResponseImages }
|
||||
|
||||
this.askResponse = undefined
|
||||
this.askResponseText = undefined
|
||||
this.askResponseImages = undefined
|
||||
|
|
@ -891,6 +945,8 @@ export class Cline {
|
|||
}
|
||||
|
||||
const block = cloneDeep(this.assistantMessageContent[this.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
|
||||
|
||||
|
||||
switch (block.type) {
|
||||
case "text": {
|
||||
if (this.didRejectTool || this.didAlreadyUseTool) {
|
||||
|
|
@ -1049,10 +1105,9 @@ export class Cline {
|
|||
|
||||
const handleError = async (action: string, error: Error) => {
|
||||
const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
|
||||
await this.say(
|
||||
"error",
|
||||
`Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
|
||||
)
|
||||
const errorMessage = `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
|
||||
await this.say("error", errorMessage)
|
||||
await this.notifySlack('fail', errorMessage)
|
||||
// this.toolResults.push({
|
||||
// type: "tool_result",
|
||||
// tool_use_id: toolUseId,
|
||||
|
|
@ -2003,13 +2058,62 @@ export class Cline {
|
|||
commandResult = execCommandResult
|
||||
} else {
|
||||
await this.say("completion_result", result, undefined, false)
|
||||
await (async () => {
|
||||
try {
|
||||
if (!result) {
|
||||
console.warn("No result provided for completion notification", {
|
||||
taskId: this.taskId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
console.log("About to call notifySlack with type 'complete'", {
|
||||
resultLength: result?.length,
|
||||
taskId: this.taskId,
|
||||
hasSlackNotifier: !!this.slackNotifier,
|
||||
slackConfig: this.slackNotifier?.config,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const completionMessage = result
|
||||
? `✅ Task completed successfully!\n\nResult:\n${result}`
|
||||
: "✅ Task completed successfully!";
|
||||
|
||||
// Make sure to await the notification
|
||||
await this.notifySlack('complete', completionMessage);
|
||||
|
||||
console.log("Successfully sent completion notification to Slack", {
|
||||
taskId: this.taskId,
|
||||
messageLength: completionMessage.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error during notifySlack call:", {
|
||||
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
||||
errorStack: error instanceof Error ? error.stack : 'No stack trace',
|
||||
taskId: this.taskId
|
||||
});
|
||||
vscode.window.showErrorMessage(`Failed to send Slack completion notification: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// we already sent completion_result says, an empty string asks relinquishes control over button and field
|
||||
const { response, text, images } = await this.ask("completion_result", "", false)
|
||||
let askResponse;
|
||||
try {
|
||||
askResponse = await this.ask("completion_result", "", false);
|
||||
} catch (error) {
|
||||
console.error("Error during ask call:", {
|
||||
error,
|
||||
stack: error instanceof Error ? error.stack : 'No stack trace',
|
||||
taskId: this.taskId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const { response, text, images } = askResponse;
|
||||
if (response === "yesButtonClicked") {
|
||||
pushToolResult("") // signals to recursive loop to stop (for now this never happens since yesButtonClicked will trigger a new task)
|
||||
break
|
||||
pushToolResult("");
|
||||
break;
|
||||
}
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
|
||||
|
|
@ -2173,7 +2277,6 @@ export class Cline {
|
|||
// lastMessage.ts = Date.now() DO NOT update ts since it is used as a key for virtuoso list
|
||||
lastMessage.partial = false
|
||||
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
|
||||
console.log("updating partial message", lastMessage)
|
||||
// await this.saveClineMessages()
|
||||
}
|
||||
|
||||
|
|
@ -2240,7 +2343,6 @@ export class Cline {
|
|||
}
|
||||
|
||||
if (this.abort) {
|
||||
console.log("aborting stream...")
|
||||
if (!this.abandoned) {
|
||||
// only need to gracefully abort if this instance isn't abandoned (sometimes openrouter stream hangs, in which case this would affect future instances of cline)
|
||||
await abortStream("user_cancelled")
|
||||
|
|
|
|||
|
|
@ -89,6 +89,9 @@ type GlobalStateKey =
|
|||
| "requestDelaySeconds"
|
||||
| "currentApiConfigName"
|
||||
| "listApiConfigMeta"
|
||||
| "slackConfig"
|
||||
| "slackWebhookUrl"
|
||||
| "slackNotificationsEnabled"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
|
|
@ -234,45 +237,64 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.outputChannel.appendLine("Webview view resolved")
|
||||
}
|
||||
|
||||
async initClineWithTask(task?: string, images?: string[]) {
|
||||
await this.clearTask()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold
|
||||
} = await this.getState()
|
||||
private async initClineWithTask(task?: string, images?: string[]): Promise<void> {
|
||||
try {
|
||||
await this.clearTask();
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
slackConfig
|
||||
} = await this.getState();
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
task,
|
||||
images
|
||||
)
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
task,
|
||||
images,
|
||||
undefined,
|
||||
{
|
||||
enabled: slackConfig?.enabled ?? false,
|
||||
webhookUrl: slackConfig?.webhookUrl ?? ""
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize Cline with task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
||||
await this.clearTask()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold
|
||||
} = await this.getState()
|
||||
public async initClineWithHistoryItem(historyItem: HistoryItem): Promise<void> {
|
||||
try {
|
||||
await this.clearTask();
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
slackConfig
|
||||
} = await this.getState();
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
undefined,
|
||||
undefined,
|
||||
historyItem
|
||||
)
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
undefined,
|
||||
undefined,
|
||||
historyItem,
|
||||
{
|
||||
enabled: slackConfig?.enabled ?? false,
|
||||
webhookUrl: slackConfig?.webhookUrl ?? ""
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize Cline with history item: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Send any JSON serializable data to the react app
|
||||
|
|
@ -656,6 +678,29 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
setSoundVolume(soundVolume)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "slackNotificationsEnabled":
|
||||
const enabled = message.bool ?? false;
|
||||
try {
|
||||
await this.updateGlobalState("slackNotificationsEnabled", enabled);
|
||||
// Also update slackConfig to keep settings in sync
|
||||
const currentState = await this.getState();
|
||||
await this.updateGlobalState("slackConfig", {
|
||||
enabled: enabled,
|
||||
webhookUrl: currentState.slackWebhookUrl
|
||||
});
|
||||
await this.postStateToWebview();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Failed to update Slack notifications setting: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
break
|
||||
case "slackWebhookUrl":
|
||||
await this.updateGlobalState("slackWebhookUrl", message.text ?? "")
|
||||
await this.updateGlobalState("slackConfig", {
|
||||
enabled: (await this.getState()).slackNotificationsEnabled ?? false,
|
||||
webhookUrl: message.text ?? ""
|
||||
})
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "diffEnabled":
|
||||
const diffEnabled = message.bool ?? true
|
||||
await this.updateGlobalState("diffEnabled", diffEnabled)
|
||||
|
|
@ -912,81 +957,102 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
)
|
||||
}
|
||||
|
||||
private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
|
||||
const {
|
||||
apiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
glamaModelId,
|
||||
glamaModelInfo,
|
||||
glamaApiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
azureApiVersion,
|
||||
openAiStreamingEnabled,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
openRouterUseMiddleOutTransform,
|
||||
} = apiConfiguration
|
||||
await this.updateGlobalState("apiProvider", apiProvider)
|
||||
await this.updateGlobalState("apiModelId", apiModelId)
|
||||
await this.storeSecret("apiKey", apiKey)
|
||||
await this.updateGlobalState("glamaModelId", glamaModelId)
|
||||
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
|
||||
await this.storeSecret("glamaApiKey", glamaApiKey)
|
||||
await this.storeSecret("openRouterApiKey", openRouterApiKey)
|
||||
await this.storeSecret("awsAccessKey", awsAccessKey)
|
||||
await this.storeSecret("awsSecretKey", awsSecretKey)
|
||||
await this.storeSecret("awsSessionToken", awsSessionToken)
|
||||
await this.updateGlobalState("awsRegion", awsRegion)
|
||||
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
|
||||
await this.updateGlobalState("vertexProjectId", vertexProjectId)
|
||||
await this.updateGlobalState("vertexRegion", vertexRegion)
|
||||
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
|
||||
await this.storeSecret("openAiApiKey", openAiApiKey)
|
||||
await this.updateGlobalState("openAiModelId", openAiModelId)
|
||||
await this.updateGlobalState("ollamaModelId", ollamaModelId)
|
||||
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
|
||||
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
|
||||
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
|
||||
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
|
||||
await this.storeSecret("geminiApiKey", geminiApiKey)
|
||||
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
|
||||
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
|
||||
await this.updateGlobalState("azureApiVersion", azureApiVersion)
|
||||
await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
|
||||
await this.updateGlobalState("openRouterModelId", openRouterModelId)
|
||||
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
|
||||
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
|
||||
if (this.cline) {
|
||||
this.cline.api = buildApiHandler(apiConfiguration)
|
||||
}
|
||||
private async updateApiConfiguration(apiConfiguration: ApiConfiguration): Promise<void> {
|
||||
try {
|
||||
// Destructure all configuration values
|
||||
const {
|
||||
apiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
glamaModelId,
|
||||
glamaModelInfo,
|
||||
glamaApiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
azureApiVersion,
|
||||
openAiStreamingEnabled,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
openRouterUseMiddleOutTransform,
|
||||
} = apiConfiguration;
|
||||
|
||||
// Update all state values in parallel for better performance
|
||||
await Promise.all([
|
||||
// Update global state values
|
||||
this.updateGlobalState("apiProvider", apiProvider),
|
||||
this.updateGlobalState("apiModelId", apiModelId),
|
||||
this.updateGlobalState("glamaModelId", glamaModelId),
|
||||
this.updateGlobalState("glamaModelInfo", glamaModelInfo),
|
||||
this.updateGlobalState("awsRegion", awsRegion),
|
||||
this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference),
|
||||
this.updateGlobalState("vertexProjectId", vertexProjectId),
|
||||
this.updateGlobalState("vertexRegion", vertexRegion),
|
||||
this.updateGlobalState("openAiBaseUrl", openAiBaseUrl),
|
||||
this.updateGlobalState("openAiModelId", openAiModelId),
|
||||
this.updateGlobalState("ollamaModelId", ollamaModelId),
|
||||
this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl),
|
||||
this.updateGlobalState("lmStudioModelId", lmStudioModelId),
|
||||
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
|
||||
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
|
||||
this.updateGlobalState("azureApiVersion", azureApiVersion),
|
||||
this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled),
|
||||
this.updateGlobalState("openRouterModelId", openRouterModelId),
|
||||
this.updateGlobalState("openRouterModelInfo", openRouterModelInfo),
|
||||
this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform),
|
||||
|
||||
// Store secrets
|
||||
this.storeSecret("apiKey", apiKey),
|
||||
this.storeSecret("glamaApiKey", glamaApiKey),
|
||||
this.storeSecret("openRouterApiKey", openRouterApiKey),
|
||||
this.storeSecret("awsAccessKey", awsAccessKey),
|
||||
this.storeSecret("awsSecretKey", awsSecretKey),
|
||||
this.storeSecret("awsSessionToken", awsSessionToken),
|
||||
this.storeSecret("openAiApiKey", openAiApiKey),
|
||||
this.storeSecret("geminiApiKey", geminiApiKey),
|
||||
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
|
||||
this.storeSecret("deepSeekApiKey", deepSeekApiKey)
|
||||
]);
|
||||
|
||||
// Update Cline instance if it exists
|
||||
if (this.cline) {
|
||||
this.cline.api = buildApiHandler(apiConfiguration);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update API configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
async updateCustomInstructions(instructions?: string) {
|
||||
// User may be clearing the field
|
||||
await this.updateGlobalState("customInstructions", instructions || undefined)
|
||||
if (this.cline) {
|
||||
this.cline.customInstructions = instructions || undefined
|
||||
async updateCustomInstructions(instructions?: string): Promise<void> {
|
||||
try {
|
||||
// User may be clearing the field
|
||||
const normalizedInstructions = instructions || undefined;
|
||||
await this.updateGlobalState("customInstructions", normalizedInstructions);
|
||||
|
||||
if (this.cline) {
|
||||
this.cline.customInstructions = normalizedInstructions;
|
||||
}
|
||||
|
||||
await this.postStateToWebview();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update custom instructions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
// MCP
|
||||
|
|
@ -1332,12 +1398,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
|
||||
if (fileExists) {
|
||||
const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
|
||||
return {
|
||||
historyItem,
|
||||
taskDirPath,
|
||||
apiConversationHistoryFilePath,
|
||||
uiMessagesFilePath,
|
||||
apiConversationHistory,
|
||||
return {
|
||||
historyItem,
|
||||
taskDirPath,
|
||||
apiConversationHistoryFilePath,
|
||||
uiMessagesFilePath,
|
||||
apiConversationHistory,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1362,15 +1428,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
}
|
||||
|
||||
async deleteTaskWithId(id: string) {
|
||||
if (id === this.cline?.taskId) {
|
||||
if (id === this.cline?.taskId) {
|
||||
await this.clearTask()
|
||||
}
|
||||
}
|
||||
|
||||
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
|
||||
|
||||
await this.deleteTaskFromState(id)
|
||||
|
||||
// Delete the task files
|
||||
// Delete the task files
|
||||
const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
|
||||
if (apiConversationHistoryFileExists) {
|
||||
await fs.unlink(apiConversationHistoryFilePath)
|
||||
|
|
@ -1387,12 +1453,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
}
|
||||
|
||||
async deleteTaskFromState(id: string) {
|
||||
// Remove the task from history
|
||||
// Remove the task from history
|
||||
const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
|
||||
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
|
||||
await this.updateGlobalState("taskHistory", updatedTaskHistory)
|
||||
|
||||
// Notify the webview that the task has been deleted
|
||||
// Notify the webview that the task has been deleted
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
|
|
@ -1426,6 +1492,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
requestDelaySeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
slackWebhookUrl,
|
||||
slackNotificationsEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
const allowedCommands = vscode.workspace
|
||||
|
|
@ -1462,12 +1530,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
requestDelaySeconds: requestDelaySeconds ?? 5,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
slackWebhookUrl: slackWebhookUrl ?? "",
|
||||
slackNotificationsEnabled: slackNotificationsEnabled ?? false,
|
||||
slackConfig: {
|
||||
enabled: slackNotificationsEnabled ?? false,
|
||||
webhookUrl: slackWebhookUrl ?? ""
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async clearTask() {
|
||||
this.cline?.abortTask()
|
||||
this.cline = undefined // removes reference to it, so once promises end it will be garbage collected
|
||||
this.cline = undefined // removes reference to it, so once promises end it will be garbage collected
|
||||
}
|
||||
|
||||
// Caching mechanism to keep track of webview messages + API conversation history per provider instance
|
||||
|
|
@ -1516,7 +1590,38 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
https://www.eliostruyf.com/devhack-code-extension-storage-options/
|
||||
*/
|
||||
|
||||
async getState() {
|
||||
public async getState(): Promise<{
|
||||
apiConfiguration: ApiConfiguration;
|
||||
lastShownAnnouncementId?: string;
|
||||
customInstructions?: string;
|
||||
alwaysAllowReadOnly: boolean;
|
||||
alwaysAllowWrite: boolean;
|
||||
alwaysAllowExecute: boolean;
|
||||
alwaysAllowBrowser: boolean;
|
||||
alwaysAllowMcp: boolean;
|
||||
taskHistory?: HistoryItem[];
|
||||
allowedCommands?: string[];
|
||||
soundEnabled: boolean;
|
||||
diffEnabled: boolean;
|
||||
soundVolume?: number;
|
||||
browserViewportSize: string;
|
||||
screenshotQuality: number;
|
||||
fuzzyMatchThreshold: number;
|
||||
writeDelayMs: number;
|
||||
terminalOutputLineLimit: number;
|
||||
slackWebhookUrl: string;
|
||||
slackNotificationsEnabled: boolean;
|
||||
slackConfig: {
|
||||
enabled: boolean;
|
||||
webhookUrl: string;
|
||||
};
|
||||
preferredLanguage: string;
|
||||
mcpEnabled: boolean;
|
||||
alwaysApproveResubmit: boolean;
|
||||
requestDelaySeconds: number;
|
||||
currentApiConfigName: string;
|
||||
listApiConfigMeta: ApiConfigMeta[];
|
||||
}> {
|
||||
const [
|
||||
storedApiProvider,
|
||||
apiModelId,
|
||||
|
|
@ -1571,6 +1676,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
requestDelaySeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
slackWebhookUrl,
|
||||
slackNotificationsEnabled,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||
|
|
@ -1625,6 +1732,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
|
||||
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
|
||||
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
|
||||
this.getGlobalState("slackWebhookUrl") as Promise<string | undefined>,
|
||||
this.getGlobalState("slackNotificationsEnabled") as Promise<boolean | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
|
|
@ -1691,6 +1800,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
|
||||
writeDelayMs: writeDelayMs ?? 1000,
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
slackWebhookUrl: slackWebhookUrl ?? "",
|
||||
slackNotificationsEnabled: slackNotificationsEnabled ?? false,
|
||||
slackConfig: {
|
||||
enabled: slackNotificationsEnabled ?? false,
|
||||
webhookUrl: slackWebhookUrl ?? ""
|
||||
},
|
||||
preferredLanguage: preferredLanguage ?? (() => {
|
||||
// Get VSCode's locale setting
|
||||
const vscodeLang = vscode.env.language;
|
||||
|
|
@ -1789,25 +1904,25 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
for (const key of this.context.globalState.keys()) {
|
||||
await this.context.globalState.update(key, undefined)
|
||||
}
|
||||
const secretKeys: SecretKey[] = [
|
||||
"apiKey",
|
||||
"glamaApiKey",
|
||||
"openRouterApiKey",
|
||||
"awsAccessKey",
|
||||
"awsSecretKey",
|
||||
"awsSessionToken",
|
||||
"openAiApiKey",
|
||||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
"deepSeekApiKey",
|
||||
const secretKeys: SecretKey[] = [
|
||||
"apiKey",
|
||||
"glamaApiKey",
|
||||
"openRouterApiKey",
|
||||
"awsAccessKey",
|
||||
"awsSecretKey",
|
||||
"awsSessionToken",
|
||||
"openAiApiKey",
|
||||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
"deepSeekApiKey",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
await this.storeSecret(key, undefined)
|
||||
}
|
||||
if (this.cline) {
|
||||
if (this.cline) {
|
||||
this.cline.abortTask()
|
||||
this.cline = undefined
|
||||
}
|
||||
}
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
await this.postStateToWebview()
|
||||
await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
|
|
|
|||
94
src/services/slack/index.ts
Normal file
94
src/services/slack/index.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import * as vscode from 'vscode'
|
||||
|
||||
export interface SlackConfig {
|
||||
webhookUrl: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export class SlackNotifier {
|
||||
public readonly config: SlackConfig
|
||||
|
||||
constructor(config: SlackConfig) {
|
||||
console.log("Creating new SlackNotifier instance with config:", {
|
||||
enabled: config.enabled,
|
||||
hasWebhookUrl: !!config.webhookUrl,
|
||||
webhookUrlLength: config.webhookUrl?.length
|
||||
});
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private async sendMessage(text: string): Promise<void> {
|
||||
console.log("SlackNotifier.sendMessage called with:", {
|
||||
text,
|
||||
config: {
|
||||
enabled: this.config.enabled,
|
||||
hasWebhookUrl: !!this.config.webhookUrl,
|
||||
webhookUrlLength: this.config.webhookUrl?.length
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.config.enabled) {
|
||||
console.log("Slack notifications are disabled in config");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config.webhookUrl) {
|
||||
console.log("No Slack webhook URL configured in config");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Preparing Slack webhook request...");
|
||||
const body = JSON.stringify({ text });
|
||||
console.log("Request body prepared:", { bodyLength: body.length });
|
||||
|
||||
console.log("Sending request to Slack webhook...");
|
||||
const response = await fetch(this.config.webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ text })
|
||||
})
|
||||
|
||||
console.log("Received response from Slack webhook:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
console.error("Slack API error response:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
responseText,
|
||||
webhookUrlLength: this.config.webhookUrl.length,
|
||||
messageLength: text.length
|
||||
});
|
||||
throw new Error(`Failed to send Slack message: ${response.statusText} (${response.status})`);
|
||||
}
|
||||
|
||||
console.log("Successfully sent Slack message:", {
|
||||
messageLength: text.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending Slack notification:', error);
|
||||
vscode.window.showErrorMessage(`Failed to send Slack notification: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
// Don't throw - we don't want Slack errors to interrupt the main flow
|
||||
}
|
||||
}
|
||||
|
||||
async notifyTaskComplete(taskDescription: string): Promise<void> {
|
||||
await this.sendMessage(`✅ Task Complete: ${taskDescription}`)
|
||||
}
|
||||
|
||||
async notifyUserInputNeeded(question: string): Promise<void> {
|
||||
await this.sendMessage(`❓ User Input Received: ${question}`)
|
||||
}
|
||||
|
||||
async notifyTaskFailed(error: string): Promise<void> {
|
||||
await this.sendMessage(`❌ Task Failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,13 @@ export interface ExtensionMessage {
|
|||
| "enhancedPrompt"
|
||||
| "commitSearchResults"
|
||||
| "listApiConfig"
|
||||
| "soundEnabled"
|
||||
| "soundVolume"
|
||||
| "slackNotificationsEnabled"
|
||||
| "slackWebhookUrl"
|
||||
text?: string
|
||||
bool?: boolean
|
||||
value?: number
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
| "mcpButtonClicked"
|
||||
|
|
@ -59,6 +65,12 @@ export interface ExtensionState {
|
|||
apiConfiguration?: ApiConfiguration
|
||||
currentApiConfigName?: string
|
||||
listApiConfigMeta?: ApiConfigMeta[]
|
||||
slackWebhookUrl?: string
|
||||
slackNotificationsEnabled?: boolean
|
||||
slackConfig?: {
|
||||
webhookUrl: string
|
||||
enabled: boolean
|
||||
}
|
||||
customInstructions?: string
|
||||
alwaysAllowReadOnly?: boolean
|
||||
alwaysAllowWrite?: boolean
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export interface WebviewMessage {
|
|||
| "soundEnabled"
|
||||
| "soundVolume"
|
||||
| "diffEnabled"
|
||||
| "slackNotificationsEnabled"
|
||||
| "slackWebhookUrl"
|
||||
| "browserViewportSize"
|
||||
| "screenshotQuality"
|
||||
| "openMcpSettings"
|
||||
|
|
|
|||
77
src/test/slack.test.ts
Normal file
77
src/test/slack.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { SlackNotifier } from '../services/slack'
|
||||
|
||||
describe('SlackNotifier', () => {
|
||||
let slackNotifier: SlackNotifier
|
||||
let mockFetch: jest.Mock
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch = jest.fn()
|
||||
global.fetch = mockFetch
|
||||
slackNotifier = new SlackNotifier({
|
||||
webhookUrl: 'https://hooks.slack.com/services/test',
|
||||
enabled: true
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('should send task completion notification', async () => {
|
||||
await slackNotifier.notifyTaskComplete('Task completed successfully')
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://hooks.slack.com/services/test',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: '✅ Task Complete: Task completed successfully' })
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should send user input needed notification', async () => {
|
||||
await slackNotifier.notifyUserInputNeeded('What is your preference?')
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://hooks.slack.com/services/test',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: '❓ User Input Received: What is your preference?' })
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should send task failed notification', async () => {
|
||||
await slackNotifier.notifyTaskFailed('Error occurred')
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://hooks.slack.com/services/test',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: '❌ Task Failed: Error occurred' })
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should not send notifications when disabled', async () => {
|
||||
slackNotifier = new SlackNotifier({
|
||||
webhookUrl: 'https://hooks.slack.com/services/test',
|
||||
enabled: false
|
||||
})
|
||||
|
||||
await slackNotifier.notifyTaskComplete('Task completed')
|
||||
await slackNotifier.notifyUserInputNeeded('Input needed')
|
||||
await slackNotifier.notifyTaskFailed('Task failed')
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle fetch errors gracefully', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
// These should not throw errors
|
||||
await expect(slackNotifier.notifyTaskComplete('Task completed')).resolves.not.toThrow()
|
||||
await expect(slackNotifier.notifyUserInputNeeded('Input needed')).resolves.not.toThrow()
|
||||
await expect(slackNotifier.notifyTaskFailed('Task failed')).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
|
|
@ -327,8 +327,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
|
||||
break
|
||||
case "completion_result":
|
||||
// First send the completion approval, then start new task
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" }); // Increase delay to ensure notification is processed
|
||||
break;
|
||||
case "resume_completed_task":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
startNewTask()
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,18 +58,26 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
setRequestDelaySeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
slackWebhookUrl,
|
||||
setSlackWebhookUrl,
|
||||
slackNotificationsEnabled,
|
||||
setSlackNotificationsEnabled,
|
||||
} = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [commandInput, setCommandInput] = useState("")
|
||||
|
||||
const handleSubmit = () => {
|
||||
console.log('handleSubmit called');
|
||||
console.log('Validating configuration...');
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(apiConfiguration, glamaModels, openRouterModels)
|
||||
|
||||
console.log('Validation results:', { apiValidationResult, modelIdValidationResult });
|
||||
setApiErrorMessage(apiValidationResult)
|
||||
setModelIdErrorMessage(modelIdValidationResult)
|
||||
if (!apiValidationResult && !modelIdValidationResult) {
|
||||
console.log('Validation passed, sending messages...');
|
||||
vscode.postMessage({
|
||||
type: "apiConfiguration",
|
||||
apiConfiguration
|
||||
|
|
@ -94,6 +102,17 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
|
||||
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
|
||||
vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
|
||||
// Send settings to extension
|
||||
console.log('Sending settings to extension...');
|
||||
console.log('Sound enabled:', soundEnabled);
|
||||
console.log('Sound volume:', soundVolume);
|
||||
console.log('Slack notifications enabled:', slackNotificationsEnabled);
|
||||
console.log('Slack webhook URL:', slackWebhookUrl);
|
||||
|
||||
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled });
|
||||
vscode.postMessage({ type: "soundVolume", value: soundVolume });
|
||||
vscode.postMessage({ type: "slackNotificationsEnabled", bool: slackNotificationsEnabled });
|
||||
vscode.postMessage({ type: "slackWebhookUrl", text: slackWebhookUrl });
|
||||
vscode.postMessage({
|
||||
type: "upsertApiConfiguration",
|
||||
text: currentApiConfigName,
|
||||
|
|
@ -157,7 +176,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
}}>
|
||||
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
|
||||
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
|
||||
<VSCodeButton onClick={() => {
|
||||
handleSubmit();
|
||||
}}>Done</VSCodeButton>
|
||||
</div>
|
||||
<div
|
||||
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
|
||||
|
|
@ -641,6 +662,41 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 15 }}>
|
||||
<VSCodeCheckbox checked={slackNotificationsEnabled ?? false} onChange={(e: any) => setSlackNotificationsEnabled(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Enable Slack notifications</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
When enabled, Cline will send notifications to Slack for important events.
|
||||
</p>
|
||||
|
||||
{slackNotificationsEnabled && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeTextField
|
||||
value={slackWebhookUrl}
|
||||
style={{ width: "100%" }}
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
onInput={(e: any) => {
|
||||
setSlackWebhookUrl(e.target.value)
|
||||
vscode.postMessage({ type: "slackWebhookUrl", text: e.target.value })
|
||||
}}>
|
||||
<span style={{ fontWeight: "500" }}>Slack Webhook URL</span>
|
||||
</VSCodeTextField>
|
||||
<p style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Enter your Slack Incoming Webhook URL to receive notifications.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setCurrentApiConfigName: (value: string) => void
|
||||
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
|
||||
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
|
||||
setSlackWebhookUrl: (value: string) => void
|
||||
setSlackNotificationsEnabled: (value: boolean) => void
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
|
|
@ -81,6 +83,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
requestDelaySeconds: 5,
|
||||
currentApiConfigName: 'default',
|
||||
listApiConfigMeta: [],
|
||||
slackWebhookUrl: "",
|
||||
slackNotificationsEnabled: false,
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
|
|
@ -107,8 +111,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
})
|
||||
}, [state])
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
|
||||
const message = event.data
|
||||
switch (message.type) {
|
||||
case "state": {
|
||||
setState(message.state!)
|
||||
|
|
@ -171,6 +175,25 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setListApiConfigMeta(message.listApiConfig ?? [])
|
||||
break
|
||||
}
|
||||
case "soundEnabled": {
|
||||
if (message.bool !== undefined) {
|
||||
setState((prevState: ExtensionState) => ({ ...prevState, soundEnabled: message.bool }))
|
||||
}
|
||||
break
|
||||
}
|
||||
case "soundVolume": {
|
||||
if (message.value !== undefined) {
|
||||
setState((prevState: ExtensionState) => ({ ...prevState, soundVolume: message.value }))
|
||||
}
|
||||
break
|
||||
}
|
||||
case "slackWebhookUrl": {
|
||||
if (message.text !== undefined) {
|
||||
setState((prevState: ExtensionState) => ({ ...prevState, slackWebhookUrl: message.text }))
|
||||
vscode.postMessage({ type: "slackWebhookUrl", text: message.text })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [setListApiConfigMeta])
|
||||
|
||||
|
|
@ -194,6 +217,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||
writeDelayMs: state.writeDelayMs,
|
||||
screenshotQuality: state.screenshotQuality,
|
||||
slackWebhookUrl: state.slackWebhookUrl,
|
||||
slackNotificationsEnabled: state.slackNotificationsEnabled,
|
||||
setApiConfiguration: (value) => setState((prevState) => ({
|
||||
...prevState,
|
||||
apiConfiguration: value
|
||||
|
|
@ -220,7 +245,15 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
|
||||
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
|
||||
setListApiConfigMeta,
|
||||
onUpdateApiConfig
|
||||
onUpdateApiConfig,
|
||||
setSlackWebhookUrl: (value) => {
|
||||
setState((prevState) => ({ ...prevState, slackWebhookUrl: value }))
|
||||
vscode.postMessage({ type: "slackWebhookUrl", text: value })
|
||||
},
|
||||
setSlackNotificationsEnabled: (value) => {
|
||||
setState((prevState) => ({ ...prevState, slackNotificationsEnabled: value }))
|
||||
vscode.postMessage({ type: "slackNotificationsEnabled", bool: value })
|
||||
}
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue