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:
Tom Rochette
2026-05-21 16:35:42 +03:00
committed by GitHub
parent 8dac0f73c0
commit 6cc1afc963
29 changed files with 1590 additions and 564 deletions
@@ -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 };