feat: expand magic prompts coverage and split generation prompts (#835)
- Add configurable visible/instructions prompt families for commit/PR generation, PR checks/comments flows, and git conflict resolution helpers. - Refactor prompt sending to explicit visible + synthetic parts instead of newline-based splitting, with legacy override migration for old keys. - Polish Magic Prompts settings UX with grouped sidebar entries, tooltip-based descriptions, AI icon, and validation that visible prompts cannot be empty across web and VS Code runtimes.
This commit is contained in:
committed by
GitHub
parent
2e5b02e753
commit
5f0d1623ae
@@ -0,0 +1,63 @@
|
||||
import { createMagicPromptRuntime } from './runtime.js';
|
||||
|
||||
export const registerMagicPromptRoutes = (app, dependencies) => {
|
||||
const {
|
||||
fsPromises,
|
||||
path,
|
||||
openchamberDataDir,
|
||||
} = dependencies;
|
||||
|
||||
const runtime = createMagicPromptRuntime({
|
||||
fsPromises,
|
||||
path,
|
||||
filePath: path.join(openchamberDataDir, 'magic-prompts.json'),
|
||||
});
|
||||
|
||||
app.get('/api/magic-prompts', async (_req, res) => {
|
||||
try {
|
||||
const state = await runtime.readPromptState();
|
||||
res.json(state);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to read magic prompts' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/magic-prompts/:id', async (req, res) => {
|
||||
const id = typeof req.params?.id === 'string' ? req.params.id : '';
|
||||
const text = typeof req.body?.text === 'string' ? req.body.text : null;
|
||||
if (text === null) {
|
||||
return res.status(400).json({ error: 'text is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const state = await runtime.setOverride(id, text);
|
||||
return res.json(state);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const status = message.includes('Invalid prompt id') || message.includes('too long') || message.includes('cannot be empty') ? 400 : 500;
|
||||
return res.status(status).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/magic-prompts/:id', async (req, res) => {
|
||||
const id = typeof req.params?.id === 'string' ? req.params.id : '';
|
||||
try {
|
||||
const state = await runtime.resetOverride(id);
|
||||
return res.json(state);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const status = message.includes('Invalid prompt id') ? 400 : 500;
|
||||
return res.status(status).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/magic-prompts', async (_req, res) => {
|
||||
try {
|
||||
const state = await runtime.resetAllOverrides();
|
||||
return res.json(state);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return res.status(500).json({ error: message || 'Failed to reset magic prompts' });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
const FILE_VERSION = 1;
|
||||
const MAX_PROMPT_TEXT_LENGTH = 200_000;
|
||||
const PROMPT_ID_PATTERN = /^[a-z0-9._-]{1,160}$/;
|
||||
const isVisiblePromptID = (id) => typeof id === 'string' && id.endsWith('.visible');
|
||||
|
||||
const hasOwn = (input, key) => Object.prototype.hasOwnProperty.call(input, key);
|
||||
|
||||
const sanitizeOverrides = (value) => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const next = {};
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
if (!PROMPT_ID_PATTERN.test(key) || typeof entry !== 'string') {
|
||||
continue;
|
||||
}
|
||||
next[key] = entry;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
export const createMagicPromptRuntime = (dependencies) => {
|
||||
const {
|
||||
fsPromises,
|
||||
path,
|
||||
filePath,
|
||||
} = dependencies;
|
||||
|
||||
let writeLock = Promise.resolve();
|
||||
|
||||
const readPromptState = async () => {
|
||||
try {
|
||||
const raw = await fsPromises.readFile(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
const overrides = sanitizeOverrides(parsed?.overrides);
|
||||
return {
|
||||
version: FILE_VERSION,
|
||||
overrides,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
||||
return { version: FILE_VERSION, overrides: {} };
|
||||
}
|
||||
console.warn('Failed to read magic prompts file:', error);
|
||||
return { version: FILE_VERSION, overrides: {} };
|
||||
}
|
||||
};
|
||||
|
||||
const writePromptState = async (state) => {
|
||||
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fsPromises.writeFile(filePath, JSON.stringify(state, null, 2), 'utf8');
|
||||
};
|
||||
|
||||
const persist = async (mutator) => {
|
||||
const run = async () => {
|
||||
const current = await readPromptState();
|
||||
const next = await mutator(current);
|
||||
await writePromptState(next);
|
||||
return next;
|
||||
};
|
||||
writeLock = writeLock.then(run, run);
|
||||
return writeLock;
|
||||
};
|
||||
|
||||
const setOverride = async (id, text) => {
|
||||
const normalizedId = typeof id === 'string' ? id.trim() : '';
|
||||
if (!PROMPT_ID_PATTERN.test(normalizedId)) {
|
||||
throw new Error('Invalid prompt id');
|
||||
}
|
||||
if (typeof text !== 'string') {
|
||||
throw new Error('Prompt text must be a string');
|
||||
}
|
||||
if (isVisiblePromptID(normalizedId) && text.trim().length === 0) {
|
||||
throw new Error('Visible prompt text cannot be empty');
|
||||
}
|
||||
if (text.length > MAX_PROMPT_TEXT_LENGTH) {
|
||||
throw new Error('Prompt text is too long');
|
||||
}
|
||||
|
||||
return persist(async (state) => {
|
||||
const nextOverrides = { ...state.overrides, [normalizedId]: text };
|
||||
return {
|
||||
version: FILE_VERSION,
|
||||
overrides: nextOverrides,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const resetOverride = async (id) => {
|
||||
const normalizedId = typeof id === 'string' ? id.trim() : '';
|
||||
if (!PROMPT_ID_PATTERN.test(normalizedId)) {
|
||||
throw new Error('Invalid prompt id');
|
||||
}
|
||||
|
||||
return persist(async (state) => {
|
||||
if (!hasOwn(state.overrides, normalizedId)) {
|
||||
return state;
|
||||
}
|
||||
const nextOverrides = { ...state.overrides };
|
||||
delete nextOverrides[normalizedId];
|
||||
return {
|
||||
version: FILE_VERSION,
|
||||
overrides: nextOverrides,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const resetAllOverrides = async () => {
|
||||
return persist(async () => ({ version: FILE_VERSION, overrides: {} }));
|
||||
};
|
||||
|
||||
return {
|
||||
readPromptState,
|
||||
setOverride,
|
||||
resetOverride,
|
||||
resetAllOverrides,
|
||||
};
|
||||
};
|
||||
@@ -161,6 +161,7 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
|
||||
req.path.startsWith('/api/projects') ||
|
||||
req.path.startsWith('/api/fs') ||
|
||||
req.path.startsWith('/api/git') ||
|
||||
req.path.startsWith('/api/magic-prompts') ||
|
||||
req.path.startsWith('/api/prompts') ||
|
||||
req.path.startsWith('/api/terminal') ||
|
||||
req.path.startsWith('/api/opencode') ||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { registerFsRoutes } from '../fs/routes.js';
|
||||
import { registerQuotaRoutes } from '../quota/routes.js';
|
||||
import { registerGitHubRoutes } from '../github/routes.js';
|
||||
import { registerGitRoutes } from '../git/routes.js';
|
||||
import { registerMagicPromptRoutes } from '../magic-prompts/routes.js';
|
||||
import { registerConfigEntityRoutes } from './config-entity-routes.js';
|
||||
import { registerSettingsUtilityRoutes } from './core-routes.js';
|
||||
import { registerProjectIconRoutes } from './project-icon-routes.js';
|
||||
@@ -196,6 +197,11 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
||||
registerQuotaRoutes(app, { getQuotaProviders });
|
||||
registerGitHubRoutes(app);
|
||||
registerGitRoutes(app);
|
||||
registerMagicPromptRoutes(app, {
|
||||
fsPromises,
|
||||
path,
|
||||
openchamberDataDir,
|
||||
});
|
||||
registerFsRoutes(app, {
|
||||
os,
|
||||
path,
|
||||
|
||||
Reference in New Issue
Block a user