Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { createRPCController } from '@shared/lib/ipc/rpc';
import { type PromptLibraryPrompt } from '@shared/prompt-library';
import { type PromptLibraryState } from '@shared/prompt-library';
import { promptLibraryService } from './service';

export const promptLibraryController = createRPCController({
get: (): Promise<PromptLibraryPrompt[]> => promptLibraryService.getPrompts(),
update: (prompts: PromptLibraryPrompt[]): Promise<void> =>
promptLibraryService.updatePrompts(prompts),
get: (): Promise<PromptLibraryState> => promptLibraryService.getState(),
update: (state: PromptLibraryState): Promise<void> => promptLibraryService.updateState(state),
});
37 changes: 32 additions & 5 deletions apps/emdash-desktop/src/main/core/prompt-library/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import type { IInitializable } from '@main/lib/lifecycle';
import {
DEFAULT_PROMPT_LIBRARY,
PROMPT_LIBRARY_SEED_VERSION,
promptLibraryFoldersSchema,
promptLibrarySchema,
type PromptLibraryFolder,
type PromptLibraryPrompt,
type PromptLibraryState,
} from '@shared/prompt-library';

type PromptLibraryKV = {
prompts: PromptLibraryPrompt[];
folders: PromptLibraryFolder[];
seedVersion: number;
};

Expand All @@ -26,6 +30,26 @@ export class PromptLibraryService implements IInitializable {
return parsed.success ? parsed.data : [];
}

private async readFolders(): Promise<PromptLibraryFolder[]> {
const folders = await promptLibraryKV.get('folders');
const parsed = promptLibraryFoldersSchema.safeParse(folders ?? []);
return parsed.success ? parsed.data : [];
}

// Prompts pointing at a deleted/unknown folder fall back to ungrouped instead
// of disappearing from the grouped UI.
private sanitizePrompts(
prompts: PromptLibraryPrompt[],
folders: PromptLibraryFolder[]
): PromptLibraryPrompt[] {
const folderIds = new Set(folders.map((folder) => folder.id));
return prompts.map((prompt) => {
if (!prompt.folderId || folderIds.has(prompt.folderId)) return prompt;
const { folderId: _folderId, ...rest } = prompt;
return rest;
});
}

private async readLegacyAppSetting(key: string): Promise<unknown | null> {
const rows = await db
.select({ value: appSettings.value })
Expand Down Expand Up @@ -97,14 +121,17 @@ export class PromptLibraryService implements IInitializable {
await this.seedIfNeeded();
}

async getPrompts(): Promise<PromptLibraryPrompt[]> {
async getState(): Promise<PromptLibraryState> {
await this.seedIfNeeded();
return this.readPrompts();
const [prompts, folders] = await Promise.all([this.readPrompts(), this.readFolders()]);
return { prompts: this.sanitizePrompts(prompts, folders), folders };
}

async updatePrompts(prompts: PromptLibraryPrompt[]): Promise<void> {
const validated = promptLibrarySchema.parse(prompts);
await promptLibraryKV.set('prompts', validated);
async updateState(state: PromptLibraryState): Promise<void> {
const prompts = promptLibrarySchema.parse(state.prompts);
const folders = promptLibraryFoldersSchema.parse(state.folders);
await promptLibraryKV.set('prompts', this.sanitizePrompts(prompts, folders));
await promptLibraryKV.set('folders', folders);
}
Comment thread
janburzinski marked this conversation as resolved.

async upsertReviewPrompt(prompt: string): Promise<void> {
Expand Down
2 changes: 2 additions & 0 deletions apps/emdash-desktop/src/renderer/app/modal-registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CommandPaletteModal } from '@renderer/features/command-palette/command-palette-modal';
import { IntegrationSetupModal } from '@renderer/features/integrations/integration-setup-modal';
import { PromptFolderModal } from '@renderer/features/library/prompts/prompt-folder-modal';
import { PromptModal } from '@renderer/features/library/prompts/prompt-modal';
import { McpModal } from '@renderer/features/mcp/components/McpModal';
import { AddProjectModal } from '@renderer/features/projects/components/add-project-modal/add-project-modal';
Expand Down Expand Up @@ -52,6 +53,7 @@ export const modalRegistry = {
createConversationModal: createModal(CreateConversationModal),
feedbackModal: createModal(FeedbackModal),
promptModal: createModal(PromptModal, { size: 'lg' }),
promptFolderModal: createModal(PromptFolderModal, { size: 'xs' }),
mcpServerModal: createModal(McpModal),
createSkillModal: createModal(CreateSkillModal),
conflictDialog: createModal(ConflictDialog, { size: 'sm' }),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useState } from 'react';
import type { BaseModalProps } from '@renderer/lib/modal/modal-provider';
import { Button } from '@renderer/lib/ui/button';
import { ConfirmButton } from '@renderer/lib/ui/confirm-button';
import {
DialogContentArea,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@renderer/lib/ui/dialog';
import { Field, FieldGroup, FieldLabel } from '@renderer/lib/ui/field';
import { Input } from '@renderer/lib/ui/input';

type PromptFolderModalArgs = {
initialName?: string;
existingNames?: string[];
};

type Props = BaseModalProps<string> & PromptFolderModalArgs;

export function PromptFolderModal({ initialName, existingNames = [], onSuccess, onClose }: Props) {
const [name, setName] = useState(initialName ?? '');

const normalizedName = name.trim();
const isDuplicate =
normalizedName.toLowerCase() !== (initialName ?? '').trim().toLowerCase() &&
existingNames.some(
(existing) => existing.trim().toLowerCase() === normalizedName.toLowerCase()
);
const canSave = normalizedName.length > 0 && !isDuplicate;

const handleSave = () => {
if (!canSave) return;
onSuccess(normalizedName);
};

return (
<>
<DialogHeader>
<DialogTitle>{initialName ? 'Rename Folder' : 'New Folder'}</DialogTitle>
</DialogHeader>
<DialogContentArea className="pt-0">
<FieldGroup>
<Field>
<FieldLabel>Name</FieldLabel>
<Input
data-autofocus
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave();
}}
placeholder="Reviews"
/>
{isDuplicate && (
<p className="text-destructive mt-1 text-xs">
A folder with this name already exists.
</p>
)}
</Field>
</FieldGroup>
</DialogContentArea>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<ConfirmButton onClick={handleSave} disabled={!canSave}>
{initialName ? 'Rename' : 'Create'}
</ConfirmButton>
</DialogFooter>
</>
);
}
Loading
Loading