Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/backend/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,10 @@ export const IPC_CHANNELS = {

// Settings
SETTINGS_GET_ALL_PROMPTS: "settings:get-all-prompts",
SETTINGS_GET_ACTIVE_PROMPT: "settings:get-active-prompt",
SETTINGS_GET_TEMPLATES: "settings:get-templates",
SETTINGS_ADD_PROMPT: "settings:add-prompt",
SETTINGS_UPDATE_PROMPT: "settings:update-prompt",
SETTINGS_DELETE_PROMPT: "settings:delete-prompt",
SETTINGS_SET_ACTIVE_PROMPT: "settings:set-active-prompt",
SETTINGS_CLEAR_CUSTOM_PROMPTS: "settings:clear-custom-prompts",
SETTINGS_HOTKEY_UPDATE: "settings:hotkey-update",

Expand Down
5 changes: 1 addition & 4 deletions src/backend/ipc/custom-prompt-settings-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class CustomPromptSettingsIPCHandlers {

constructor() {
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_ALL_PROMPTS, () => this.store.getAllPrompts());
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_ACTIVE_PROMPT, () => this.store.getActivePrompt());
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_TEMPLATES, () => this.store.getTemplates());
ipcMain.handle(
IPC_CHANNELS.SETTINGS_ADD_PROMPT,
(_, prompt: Omit<CustomPrompt, "id" | "createdAt" | "updatedAt">) =>
Expand All @@ -22,9 +22,6 @@ export class CustomPromptSettingsIPCHandlers {
ipcMain.handle(IPC_CHANNELS.SETTINGS_DELETE_PROMPT, (_, id: string) =>
this.store.deletePrompt(id),
);
ipcMain.handle(IPC_CHANNELS.SETTINGS_SET_ACTIVE_PROMPT, (_, id: string) =>
this.store.setActivePrompt(id),
);
ipcMain.handle(IPC_CHANNELS.SETTINGS_CLEAR_CUSTOM_PROMPTS, () =>
this.store.clearCustomPrompts(),
);
Expand Down
8 changes: 2 additions & 6 deletions src/backend/ipc/process-video-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { TranscriptionModelProvider } from "../services/mcp/transcription-model-
import { SendWorkItemDetailsToPortal, WorkItemDtoSchema } from "../services/portal/actions";
import type { ProjectDto } from "../services/prompt/prompt-manager";
import { ShaveService } from "../services/shave/shave-service";
import { CustomPromptStorage } from "../services/storage/custom-prompt-storage";
import { VideoMetadataBuilder } from "../services/video/video-metadata-builder";
import { YouTubeDownloadService } from "../services/video/youtube-service";
import { McpWorkflowAdapter } from "../services/workflow/mcp-workflow-adapter";
Expand Down Expand Up @@ -44,7 +43,6 @@ export const TranscriptSummarySchema = z.object({
export class ProcessVideoIPCHandlers {
private readonly youtube = YouTubeClient.getInstance();
private ffmpegService = FFmpegService.getInstance();
private readonly customPromptStorage = CustomPromptStorage.getInstance();
private readonly metadataBuilder: VideoMetadataBuilder;
private readonly youtubeDownloadService = YouTubeDownloadService.getInstance();
private lastVideoFilePath: string | undefined;
Expand Down Expand Up @@ -109,8 +107,7 @@ export class ProcessVideoIPCHandlers {
workflowManager.startStage(WorkflowProgressStage.EXECUTING_TASK);
notify(ProgressStage.EXECUTING_TASK);

const customPrompt = await this.customPromptStorage.getActivePrompt();
const serverFilter = customPrompt?.selectedMcpServerIds;
const serverFilter = projectDetails?.selectedMcpServerIds;

const filePath =
this.lastVideoFilePath && fs.existsSync(this.lastVideoFilePath)
Expand Down Expand Up @@ -323,8 +320,7 @@ export class ProcessVideoIPCHandlers {

notify(ProgressStage.EXECUTING_TASK, { transcriptText, intermediateOutput });

const customPrompt = await this.customPromptStorage.getActivePrompt();
const serverFilter = customPrompt?.selectedMcpServerIds;
const serverFilter = projectDetails?.selectedMcpServerIds;

const mcpAdapter = new McpWorkflowAdapter(workflowManager, {
transcriptText,
Expand Down
7 changes: 2 additions & 5 deletions src/backend/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,10 @@ const IPC_CHANNELS = {

// Settings
SETTINGS_GET_ALL_PROMPTS: "settings:get-all-prompts",
SETTINGS_GET_ACTIVE_PROMPT: "settings:get-active-prompt",
SETTINGS_GET_TEMPLATES: "settings:get-templates",
SETTINGS_ADD_PROMPT: "settings:add-prompt",
SETTINGS_UPDATE_PROMPT: "settings:update-prompt",
SETTINGS_DELETE_PROMPT: "settings:delete-prompt",
SETTINGS_SET_ACTIVE_PROMPT: "settings:set-active-prompt",
SETTINGS_CLEAR_CUSTOM_PROMPTS: "settings:clear-custom-prompts",

// General User Settings
Expand Down Expand Up @@ -269,14 +268,12 @@ const electronAPI = {
},
settings: {
getAllPrompts: () => ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_GET_ALL_PROMPTS),
getActivePrompt: () => ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_GET_ACTIVE_PROMPT),
getTemplates: () => ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_GET_TEMPLATES),
addPrompt: (prompt: { name: string; content: string }) =>
ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_ADD_PROMPT, prompt),
updatePrompt: (id: string, updates: { name?: string; content?: string }) =>
ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_UPDATE_PROMPT, id, updates),
deletePrompt: (id: string) => ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_DELETE_PROMPT, id),
setActivePrompt: (id: string) =>
ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_SET_ACTIVE_PROMPT, id),
clearCustomPrompts: () => ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_CLEAR_CUSTOM_PROMPTS),
},
userInteraction: {
Expand Down
8 changes: 8 additions & 0 deletions src/backend/services/mcp/mcp-server-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import type { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import type { ToolSet } from "ai";
import { PRESET_MCP_SERVERS } from "../../../shared/mcp/preset-servers";
import type { HealthStatusInfo } from "../../types/index.js";
import { McpStorage } from "../storage/mcp-storage";
import { type CreateClientOptions, MCPServerClient } from "./mcp-server-client";
Expand Down Expand Up @@ -229,6 +230,13 @@ export class MCPServerManager {
result.push(s);
}
}
// Add preset servers not yet stored by the user (they appear with enabled: false)
for (const preset of PRESET_MCP_SERVERS) {
if (preset.id && !seen.has(preset.id)) {
seen.add(preset.id);
result.push({ ...preset });
}
}
return result;
}

Expand Down
11 changes: 4 additions & 7 deletions src/backend/services/prompt/prompt-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export interface PromptSummary {
id: string;
name: string;
description?: string;
isActive: boolean;
source: "local" | "remote";
}

Expand All @@ -36,6 +35,7 @@ export interface ProjectDto {
gitHubProjectId?: string;
placeItemOnTopOfProductBacklog: boolean;
desktopAgentProjectPrompt?: string;
selectedMcpServerIds?: string[];
}

export class PromptManager {
Expand Down Expand Up @@ -79,8 +79,7 @@ export class PromptManager {
id: p.id,
name: p.name,
description: p.description,
isActive: true,
source: "local",
source: "local" as const,
}));
} catch (error) {
console.error("Failed to fetch local prompts:", error);
Expand Down Expand Up @@ -160,8 +159,7 @@ export class PromptManager {
id: item.id,
name: item.title,
description: item.description,
isActive: true,
source: "remote",
source: "remote" as const,
}));
} catch (error) {
console.error("Failed to fetch remote prompts:", error);
Expand All @@ -183,9 +181,8 @@ export class PromptManager {
id: localPrompt.id,
name: localPrompt.name,
description: localPrompt.description,
// Map local prompt content to desktopAgentProjectPrompt
desktopAgentProjectPrompt: localPrompt.content,
// Set defaults for other required fields
selectedMcpServerIds: localPrompt.selectedMcpServerIds,
videoHostType: "SharePoint", // Default
recentWorkItemsCount: 0,
allowWebhooks: false,
Expand Down
69 changes: 28 additions & 41 deletions src/backend/services/storage/custom-prompt-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,30 @@ export interface CustomPrompt {
name: string;
description?: string;
content: string;
isDefault?: boolean;
isTemplate?: boolean;
selectedMcpServerIds?: string[];
createdAt: number;
updatedAt: number;
}

interface CustomPromptData {
prompts: CustomPrompt[];
activePromptId: string | null;
}

const SETTINGS_FILE = "custom-settings.enc";

const DEFAULT_PROMPT: CustomPrompt = {
const TEMPLATE_PROMPT: CustomPrompt = {
id: "default",
name: "Default Prompt",
description: "This is the default prompt for YakShaver",
content: defaultCustomPrompt,

isDefault: true,
name: "Create Issues Template",
description: "Template for creating issues from video recordings",
content: `Project Name: <REPLACE WITH YOUR PROJECT NAME>\nProject URL: <REPLACE WITH REPO OR BOARD URL>\n${defaultCustomPrompt}`,
isTemplate: true,
createdAt: Date.now(),
updatedAt: Date.now(),
};

const DEFAULT_SETTINGS: CustomPromptData = {
prompts: [DEFAULT_PROMPT],
activePromptId: "default",
prompts: [TEMPLATE_PROMPT],
};

export class CustomPromptStorage extends BaseSecureStorage {
Expand Down Expand Up @@ -63,15 +60,22 @@ export class CustomPromptStorage extends BaseSecureStorage {
const data = await this.decryptAndLoad<CustomPromptData>(this.getSettingsPath());
this.cache = data || DEFAULT_SETTINGS;

// Migrate default prompt to new default if there are changes
// Migrate the built-in template prompt when content or metadata changes
if (this.cache) {
const defaultPromptIndex = this.cache.prompts.findIndex((p) => p.id === "default");
if (defaultPromptIndex !== -1) {
const currentDefaultPrompt = this.cache.prompts[defaultPromptIndex];
if (currentDefaultPrompt.content !== DEFAULT_PROMPT.content) {
this.cache.prompts[defaultPromptIndex] = {
...currentDefaultPrompt,
content: DEFAULT_PROMPT.content,
const templateIndex = this.cache.prompts.findIndex((p) => p.id === "default");
if (templateIndex !== -1) {
const current = this.cache.prompts[templateIndex];
const needsUpdate =
current.content !== TEMPLATE_PROMPT.content ||
current.name !== TEMPLATE_PROMPT.name ||
!current.isTemplate;
if (needsUpdate) {
this.cache.prompts[templateIndex] = {
...current,
content: TEMPLATE_PROMPT.content,
name: TEMPLATE_PROMPT.name,
description: TEMPLATE_PROMPT.description,
isTemplate: true,
updatedAt: Date.now(),
};
await this.saveSettings(this.cache);
Expand All @@ -89,13 +93,12 @@ export class CustomPromptStorage extends BaseSecureStorage {

async getAllPrompts(): Promise<CustomPrompt[]> {
const settings = await this.loadSettings();
return settings.prompts;
return settings.prompts.filter((p) => !p.isTemplate);
}

async getActivePrompt(): Promise<CustomPrompt | null> {
async getTemplates(): Promise<CustomPrompt[]> {
const settings = await this.loadSettings();
if (!settings.activePromptId) return null;
return settings.prompts.find((p) => p.id === settings.activePromptId) || null;
return settings.prompts.filter((p) => p.isTemplate);
}

async getPromptById(id: string): Promise<CustomPrompt | null> {
Expand Down Expand Up @@ -144,35 +147,19 @@ export class CustomPromptStorage extends BaseSecureStorage {
const settings = await this.loadSettings();
const prompt = settings.prompts.find((p) => p.id === id);

// Prevent deleting default prompt
if (!prompt || prompt.isDefault) return false;
// Prevent deleting template or default prompts
if (!prompt || prompt.isTemplate) return false;

settings.prompts = settings.prompts.filter((p) => p.id !== id);

// If deleted prompt was active, switch to default
if (settings.activePromptId === id) {
settings.activePromptId = "default";
}

await this.saveSettings(settings);
return true;
}

async setActivePrompt(id: string): Promise<boolean> {
const settings = await this.loadSettings();
const prompt = settings.prompts.find((p) => p.id === id);
if (!prompt) return false;

settings.activePromptId = id;
await this.saveSettings(settings);
return true;
}

async clearCustomPrompts(): Promise<void> {
const settings = await this.loadSettings();

settings.prompts = [DEFAULT_PROMPT];
settings.activePromptId = DEFAULT_PROMPT.id;
settings.prompts = [TEMPLATE_PROMPT];

await this.saveSettings(settings);
}
Expand Down
4 changes: 2 additions & 2 deletions src/backend/services/storage/default-custom-prompt.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export const defaultCustomPrompt = `
You need to create issues or Product Backlog Items (PBIs) on task management platforms such as GitHub or Azure DevOps.
You need to create issues or Product Backlog Items (PBIs) on task management platforms such as GitHub, Azure DevOps, Jira and etc.
You will be provided with a **User Video Transcription**, and a list of tools. Your goal is to identify the correct project and create an issue with the appropriate content using these tools.

1) **Identify the Platform**: Using the transcription determine which platform the user intends to use (e.g., GitHub, Azure DevOps, Jira). If the platform is unclear, use the provided tools to investigate.
1) **Identify the Platform**: Using the transcription or repository link, determine which platform the user intends to use (e.g., GitHub, Azure DevOps, Jira). If the platform is unclear, use the provided tools to investigate.
2) **Identify the Project/Repository**: Determine the specific project or repository where the issue should be created, then using the tools to verify the details. If there's not exact match, use the provided tools to find out the possible closest match.
3) **Follow Issue Templates**: If the target repository has an issue template, you MUST follow it exactly. Use the available tools to verify if a template exists.

Expand Down
57 changes: 57 additions & 0 deletions src/shared/mcp/preset-servers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { MCPServerConfig } from "../types/mcp";

/**
* IDs for well-known preset MCP servers.
*/
export const PRESET_SERVER_IDS = {
GITHUB: "f12980ac-f80c-47e0-b4ac-181a54122d61",
AZURE_DEVOPS: "483d49a4-0902-415a-a987-832a21bd3d63",
JIRA: "0f03a50c-219b-46e9-9ce3-54f925c44479",
} as const;

/** Default config for the GitHub preset MCP server. */
export const GITHUB_PRESET_CONFIG = {
id: PRESET_SERVER_IDS.GITHUB,
name: "GitHub",
transport: "streamableHttp",
url: "https://api.githubcopilot.com/mcp/",
description: "GitHub MCP Server",
toolWhitelist: [],
enabled: false,
} satisfies MCPServerConfig;

/** Default config for the Azure DevOps preset MCP server. */
export const AZURE_DEVOPS_PRESET_CONFIG = {
id: PRESET_SERVER_IDS.AZURE_DEVOPS,
name: "Azure_DevOps", // MCP server names cannot contain spaces
transport: "stdio",
command: "npx",
// TODO: need to be able to customize this last parameter
// https://github.com/SSWConsulting/SSW.YakShaver.Desktop/issues/547
args: ["-y", "@azure-devops/mcp", "ssw2"],
description: "Azure DevOps MCP Server",
toolWhitelist: [],
enabled: false,
} satisfies MCPServerConfig;

/** Default config for the Jira preset MCP server. */
export const JIRA_PRESET_CONFIG = {
id: PRESET_SERVER_IDS.JIRA,
name: "Jira",
transport: "streamableHttp",
url: "https://mcp.atlassian.com/v1/mcp",
description: "Atlassian MCP Server",
toolWhitelist: [],
enabled: false,
} satisfies MCPServerConfig;

/**
* All preset MCP server configs.
* Included in listAvailableServers() even before the user connects.
* Once a user saves a server with the same ID, the stored version takes precedence.
*/
export const PRESET_MCP_SERVERS: readonly MCPServerConfig[] = [
GITHUB_PRESET_CONFIG,
AZURE_DEVOPS_PRESET_CONFIG,
JIRA_PRESET_CONFIG,
];
2 changes: 1 addition & 1 deletion src/ui/src/components/settings/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export function SettingsDialog() {

<section className="flex-1 h-full overflow-hidden">
<ScrollArea className="h-full pr-1">
<div className="pb-4 pr-2">
<div className="pb-4 pr-4">
{activeTab?.id === "general" && (
<GeneralSettingsPanel isActive={open && activeTabId === "general"} />
)}
Expand Down
Loading
Loading