Prompt gallery (#957)

- [x] show prompt instead of app in autocomplete
- [x] use proper array/list for db (tags)
- [x] don't do <dyad-prompt> - replace inline
This commit is contained in:
Will Chen
2025-08-18 13:25:11 -07:00
committed by GitHub
parent a547735714
commit 573642ae5f
26 changed files with 1540 additions and 42 deletions

View File

@@ -61,6 +61,9 @@ import { FileUploadsState } from "../utils/file_uploads_state";
import { OpenAIResponsesProviderOptions } from "@ai-sdk/openai";
import { extractMentionedAppsCodebases } from "../utils/mention_apps";
import { parseAppMentions } from "@/shared/parse_mention_apps";
import { prompts as promptsTable } from "../../db/schema";
import { inArray } from "drizzle-orm";
import { replacePromptReference } from "../utils/replacePromptReference";
type AsyncIterableStream<T> = AsyncIterable<T> & ReadableStream<T>;
@@ -274,6 +277,26 @@ export function registerChatStreamHandlers() {
// Add user message to database with attachment info
let userPrompt = req.prompt + (attachmentInfo ? attachmentInfo : "");
// Inline referenced prompt contents for mentions like @prompt:<id>
try {
const matches = Array.from(userPrompt.matchAll(/@prompt:(\d+)/g));
if (matches.length > 0) {
const ids = Array.from(new Set(matches.map((m) => Number(m[1]))));
const referenced = await db
.select()
.from(promptsTable)
.where(inArray(promptsTable.id, ids));
if (referenced.length > 0) {
const promptsMap: Record<number, string> = {};
for (const p of referenced) {
promptsMap[p.id] = p.content;
}
userPrompt = replacePromptReference(userPrompt, promptsMap);
}
}
} catch (e) {
logger.error("Failed to inline referenced prompts:", e);
}
if (req.selectedComponent) {
let componentSnippet = "[component snippet not available]";
try {

View File

@@ -0,0 +1,91 @@
import { IpcMainInvokeEvent } from "electron";
import log from "electron-log";
import { createLoggedHandler } from "./safe_handle";
import { db } from "@/db";
import { prompts } from "@/db/schema";
import { eq } from "drizzle-orm";
import {
CreatePromptParamsDto,
PromptDto,
UpdatePromptParamsDto,
} from "../ipc_types";
const logger = log.scope("prompt_handlers");
const handle = createLoggedHandler(logger);
export function registerPromptHandlers() {
handle("prompts:list", async (): Promise<PromptDto[]> => {
const rows = db.select().from(prompts).all();
return rows.map((r) => ({
id: r.id!,
title: r.title,
description: r.description ?? null,
content: r.content,
createdAt: r.createdAt,
updatedAt: r.updatedAt,
}));
});
handle(
"prompts:create",
async (
_e: IpcMainInvokeEvent,
params: CreatePromptParamsDto,
): Promise<PromptDto> => {
const { title, description, content } = params;
if (!title || !content) {
throw new Error("Title and content are required");
}
const result = db
.insert(prompts)
.values({
title,
description: description ?? null,
content,
})
.run();
const id = Number(result.lastInsertRowid);
const row = db.select().from(prompts).where(eq(prompts.id, id)).get();
if (!row) throw new Error("Failed to fetch created prompt");
return {
id: row.id!,
title: row.title,
description: row.description ?? null,
content: row.content,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
},
);
handle(
"prompts:update",
async (
_e: IpcMainInvokeEvent,
params: UpdatePromptParamsDto,
): Promise<void> => {
const { id, title, description, content } = params;
if (!id) throw new Error("Prompt id is required");
if (!title || !content) throw new Error("Title and content are required");
const now = new Date();
db.update(prompts)
.set({
title,
description: description ?? null,
content,
updatedAt: now,
})
.where(eq(prompts.id, id))
.run();
},
);
handle(
"prompts:delete",
async (_e: IpcMainInvokeEvent, id: number): Promise<void> => {
if (!id) throw new Error("Prompt id is required");
db.delete(prompts).where(eq(prompts.id, id)).run();
},
);
}

View File

@@ -58,6 +58,9 @@ import type {
RevertVersionResponse,
RevertVersionParams,
RespondToAppInputParams,
PromptDto,
CreatePromptParamsDto,
UpdatePromptParamsDto,
} from "./ipc_types";
import type { Template } from "../shared/templates";
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
@@ -1069,4 +1072,21 @@ export class IpcClient {
public async getTemplates(): Promise<Template[]> {
return this.ipcRenderer.invoke("get-templates");
}
// --- Prompts Library ---
public async listPrompts(): Promise<PromptDto[]> {
return this.ipcRenderer.invoke("prompts:list");
}
public async createPrompt(params: CreatePromptParamsDto): Promise<PromptDto> {
return this.ipcRenderer.invoke("prompts:create", params);
}
public async updatePrompt(params: UpdatePromptParamsDto): Promise<void> {
await this.ipcRenderer.invoke("prompts:update", params);
}
public async deletePrompt(id: number): Promise<void> {
await this.ipcRenderer.invoke("prompts:delete", id);
}
}

View File

@@ -28,6 +28,7 @@ import { registerProblemsHandlers } from "./handlers/problems_handlers";
import { registerAppEnvVarsHandlers } from "./handlers/app_env_vars_handlers";
import { registerTemplateHandlers } from "./handlers/template_handlers";
import { registerPortalHandlers } from "./handlers/portal_handlers";
import { registerPromptHandlers } from "./handlers/prompt_handlers";
export function registerIpcHandlers() {
// Register all IPC handlers by category
@@ -61,4 +62,5 @@ export function registerIpcHandlers() {
registerAppEnvVarsHandlers();
registerTemplateHandlers();
registerPortalHandlers();
registerPromptHandlers();
}

View File

@@ -353,6 +353,26 @@ export interface UploadFileToCodebaseResult {
filePath: string;
}
// --- Prompts ---
export interface PromptDto {
id: number;
title: string;
description: string | null;
content: string;
createdAt: Date;
updatedAt: Date;
}
export interface CreatePromptParamsDto {
title: string;
description?: string;
content: string;
}
export interface UpdatePromptParamsDto extends CreatePromptParamsDto {
id: number;
}
export interface FileAttachment {
file: File;
type: "upload-to-codebase" | "chat-context";

View File

@@ -0,0 +1,16 @@
export function replacePromptReference(
userPrompt: string,
promptsById: Record<number | string, string>,
): string {
if (typeof userPrompt !== "string" || userPrompt.length === 0)
return userPrompt;
return userPrompt.replace(
/@prompt:(\d+)/g,
(_match: string, idStr: string) => {
const idNum = Number(idStr);
const replacement = promptsById[idNum] ?? promptsById[idStr];
return replacement !== undefined ? replacement : _match;
},
);
}