Multi-run with configurable prompt templates (#1111)
* Multi-run with configurable prompt templates * Fixes * Fix handleDuplicate fire-and-forget: await createTemplate and handle failure * Add Polish translations for prompt template and multirun group keys * fix: migrate remaining Remix icons to Icon component in MultiRunLauncher --------- Signed-off-by: Tom Rochette <roctom@gmail.com>
This commit is contained in:
@@ -18,6 +18,11 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
||||
createMcpConfig,
|
||||
updateMcpConfig,
|
||||
deleteMcpConfig,
|
||||
listPromptTemplates,
|
||||
getPromptTemplate,
|
||||
createPromptTemplate,
|
||||
updatePromptTemplate,
|
||||
deletePromptTemplate,
|
||||
} = dependencies;
|
||||
|
||||
const completeMcpMutation = async (res, action, name, applyChange) => {
|
||||
@@ -367,4 +372,95 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
||||
res.status(500).json({ error: error.message || 'Failed to delete command' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/config/prompt-templates', async (req, res) => {
|
||||
try {
|
||||
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
const templates = listPromptTemplates(directory);
|
||||
res.json(templates);
|
||||
} catch (error) {
|
||||
console.error('[API:GET /api/config/prompt-templates] Failed:', error);
|
||||
res.status(500).json({ error: error.message || 'Failed to list prompt templates' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/config/prompt-templates/:id', async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
const template = getPromptTemplate(id, directory);
|
||||
if (!template) {
|
||||
return res.status(404).json({ error: `Prompt template "${id}" not found` });
|
||||
}
|
||||
res.json(template);
|
||||
} catch (error) {
|
||||
console.error('[API:GET /api/config/prompt-templates/:id] Failed:', error);
|
||||
res.status(500).json({ error: error.message || 'Failed to get prompt template' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/config/prompt-templates/:id', async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const config = req.body || {};
|
||||
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
console.log(`[API:POST /api/config/prompt-templates] Creating prompt template: ${id}`);
|
||||
const template = createPromptTemplate(id, config, directory);
|
||||
res.json({ success: true, template });
|
||||
} catch (error) {
|
||||
console.error('[API:POST /api/config/prompt-templates/:id] Failed:', error);
|
||||
if (error.message?.includes('already exists')) {
|
||||
return res.status(409).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message || 'Failed to create prompt template' });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/config/prompt-templates/:id', async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const updates = req.body;
|
||||
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
console.log(`[API:PATCH /api/config/prompt-templates] Updating prompt template: ${id}`);
|
||||
const template = updatePromptTemplate(id, updates, directory);
|
||||
res.json({ success: true, template });
|
||||
} catch (error) {
|
||||
console.error('[API:PATCH /api/config/prompt-templates/:id] Failed:', error);
|
||||
if (error.message?.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message || 'Failed to update prompt template' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/config/prompt-templates/:id', async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
console.log(`[API:DELETE /api/config/prompt-templates] Deleting prompt template: ${id}`);
|
||||
deletePromptTemplate(id, directory);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[API:DELETE /api/config/prompt-templates/:id] Failed:', error);
|
||||
if (error.message?.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message || 'Failed to delete prompt template' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -123,6 +123,11 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
||||
createMcpConfig,
|
||||
updateMcpConfig,
|
||||
deleteMcpConfig,
|
||||
listPromptTemplates,
|
||||
getPromptTemplate,
|
||||
createPromptTemplate,
|
||||
updatePromptTemplate,
|
||||
deletePromptTemplate,
|
||||
} = await import('./index.js');
|
||||
|
||||
registerConfigEntityRoutes(app, {
|
||||
@@ -144,6 +149,11 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
||||
createMcpConfig,
|
||||
updateMcpConfig,
|
||||
deleteMcpConfig,
|
||||
listPromptTemplates,
|
||||
getPromptTemplate,
|
||||
createPromptTemplate,
|
||||
updatePromptTemplate,
|
||||
deletePromptTemplate,
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
@@ -64,3 +64,12 @@ export {
|
||||
updateMcpConfig,
|
||||
deleteMcpConfig,
|
||||
} from './mcp.js';
|
||||
|
||||
export {
|
||||
listPromptTemplates,
|
||||
getPromptTemplate,
|
||||
createPromptTemplate,
|
||||
updatePromptTemplate,
|
||||
deletePromptTemplate,
|
||||
slugify as slugifyPromptTemplate,
|
||||
} from './prompt-templates.js';
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
readConfigLayers,
|
||||
writeConfig,
|
||||
getJsonWriteTarget,
|
||||
CONFIG_FILE,
|
||||
} from './shared.js';
|
||||
|
||||
const SECTION_KEY = 'promptTemplates';
|
||||
|
||||
const DEFAULT_TEMPLATES = {
|
||||
simple: {
|
||||
name: 'Simple',
|
||||
body: 'Implement this task using the simplest possible approach. Prefer readability and straightforward solutions over clever abstractions. Keep the code easy to understand and maintain.',
|
||||
isDefault: true,
|
||||
},
|
||||
fast: {
|
||||
name: 'Fast',
|
||||
body: 'Implement this task as quickly as possible. Optimize for speed of development. Use the most direct path to a working solution. Favor existing libraries and proven patterns.',
|
||||
isDefault: true,
|
||||
},
|
||||
'memory-efficient': {
|
||||
name: 'Memory Efficient',
|
||||
body: 'Implement this task with memory efficiency in mind. Minimize memory allocations, use streaming where possible, avoid holding large data structures in memory, and prefer lazy evaluation.',
|
||||
isDefault: true,
|
||||
},
|
||||
'cpu-efficient': {
|
||||
name: 'CPU Efficient',
|
||||
body: 'Implement this task with CPU efficiency in mind. Optimize algorithms, minimize unnecessary computations, use efficient data structures, and avoid redundant work.',
|
||||
isDefault: true,
|
||||
},
|
||||
'tests-first': {
|
||||
name: 'Tests First',
|
||||
body: 'Implement this task using a test-driven approach. Write tests first, then implement the minimum code to pass them. Ensure comprehensive test coverage including edge cases.',
|
||||
isDefault: true,
|
||||
},
|
||||
'spec-first': {
|
||||
name: 'Spec First',
|
||||
body: 'Implement this task by first creating a detailed specification, then implementing according to the spec. Start by documenting the requirements, interfaces, and expected behavior before writing any implementation code.',
|
||||
isDefault: true,
|
||||
},
|
||||
};
|
||||
|
||||
function slugify(name) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.substring(0, 60);
|
||||
}
|
||||
|
||||
function ensureDefaults(templates) {
|
||||
if (!templates || typeof templates !== 'object') {
|
||||
return { ...DEFAULT_TEMPLATES };
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const result = { ...templates };
|
||||
|
||||
for (const [id, template] of Object.entries(DEFAULT_TEMPLATES)) {
|
||||
if (!(id in result)) {
|
||||
result[id] = { ...template };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? result : templates;
|
||||
}
|
||||
|
||||
function readTemplatesFromConfig(workingDirectory) {
|
||||
const layers = readConfigLayers(workingDirectory);
|
||||
const merged = layers.mergedConfig || {};
|
||||
const raw = merged[SECTION_KEY];
|
||||
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return ensureDefaults(null);
|
||||
}
|
||||
|
||||
return ensureDefaults(raw);
|
||||
}
|
||||
|
||||
function writeTemplatesToConfig(templates, workingDirectory) {
|
||||
const layers = readConfigLayers(workingDirectory);
|
||||
const target = getJsonWriteTarget(layers, 'user');
|
||||
const config = { ...target.config };
|
||||
config[SECTION_KEY] = templates;
|
||||
writeConfig(config, target.path || CONFIG_FILE);
|
||||
}
|
||||
|
||||
export function listPromptTemplates(workingDirectory) {
|
||||
const templates = readTemplatesFromConfig(workingDirectory);
|
||||
return Object.entries(templates).map(([id, value]) => ({
|
||||
id,
|
||||
name: value.name || id,
|
||||
body: value.body || '',
|
||||
isDefault: value.isDefault === true,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getPromptTemplate(id, workingDirectory) {
|
||||
const templates = readTemplatesFromConfig(workingDirectory);
|
||||
const entry = templates[id];
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: entry.name || id,
|
||||
body: entry.body || '',
|
||||
isDefault: entry.isDefault === true,
|
||||
};
|
||||
}
|
||||
|
||||
export function createPromptTemplate(id, config, workingDirectory) {
|
||||
const templates = readTemplatesFromConfig(workingDirectory);
|
||||
|
||||
if (templates[id]) {
|
||||
throw new Error(`Prompt template "${id}" already exists`);
|
||||
}
|
||||
|
||||
templates[id] = {
|
||||
name: config.name || id,
|
||||
body: config.body || '',
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
writeTemplatesToConfig(templates, workingDirectory);
|
||||
return getPromptTemplate(id, workingDirectory);
|
||||
}
|
||||
|
||||
export function updatePromptTemplate(id, updates, workingDirectory) {
|
||||
const templates = readTemplatesFromConfig(workingDirectory);
|
||||
const existing = templates[id];
|
||||
|
||||
if (!existing) {
|
||||
throw new Error(`Prompt template "${id}" not found`);
|
||||
}
|
||||
|
||||
templates[id] = {
|
||||
...existing,
|
||||
...(updates.name !== undefined ? { name: updates.name } : {}),
|
||||
...(updates.body !== undefined ? { body: updates.body } : {}),
|
||||
};
|
||||
|
||||
writeTemplatesToConfig(templates, workingDirectory);
|
||||
return getPromptTemplate(id, workingDirectory);
|
||||
}
|
||||
|
||||
export function deletePromptTemplate(id, workingDirectory) {
|
||||
const templates = readTemplatesFromConfig(workingDirectory);
|
||||
|
||||
if (!templates[id]) {
|
||||
throw new Error(`Prompt template "${id}" not found`);
|
||||
}
|
||||
|
||||
delete templates[id];
|
||||
writeTemplatesToConfig(templates, workingDirectory);
|
||||
}
|
||||
|
||||
export { slugify };
|
||||
Reference in New Issue
Block a user