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
@@ -47,6 +47,10 @@ type BridgeMessageInput = {
|
||||
type ConfigRuntimeDeps = {
|
||||
readSettings: (ctx?: BridgeContext) => Record<string, unknown>;
|
||||
persistSettings: (changes: Record<string, unknown>, ctx?: BridgeContext) => Promise<Record<string, unknown>>;
|
||||
readMagicPromptOverrides: () => { version: number; overrides: Record<string, string> };
|
||||
saveMagicPromptOverride: (id: string, text: string) => Promise<{ version: number; overrides: Record<string, string> }>;
|
||||
resetMagicPromptOverride: (id: string) => Promise<{ version: number; overrides: Record<string, string> }>;
|
||||
resetAllMagicPromptOverrides: () => Promise<{ version: number; overrides: Record<string, string> }>;
|
||||
fetchOpenCodeSkillsFromApi: (ctx: BridgeContext | undefined, workingDirectory?: string) => Promise<DiscoveredSkill[] | null>;
|
||||
clientReloadDelayMs: number;
|
||||
};
|
||||
@@ -140,6 +144,38 @@ export async function handleConfigBridgeMessage(
|
||||
return { id, type, success: true, data: updated };
|
||||
}
|
||||
|
||||
case 'api:magic-prompts:get': {
|
||||
return { id, type, success: true, data: deps.readMagicPromptOverrides() };
|
||||
}
|
||||
|
||||
case 'api:magic-prompts:save': {
|
||||
const request = (payload || {}) as { id?: string; text?: string };
|
||||
const promptId = typeof request.id === 'string' ? request.id : '';
|
||||
if (!promptId) {
|
||||
return { id, type, success: false, error: 'Prompt id is required' };
|
||||
}
|
||||
if (typeof request.text !== 'string') {
|
||||
return { id, type, success: false, error: 'Prompt text is required' };
|
||||
}
|
||||
const data = await deps.saveMagicPromptOverride(promptId, request.text);
|
||||
return { id, type, success: true, data };
|
||||
}
|
||||
|
||||
case 'api:magic-prompts:reset': {
|
||||
const request = (payload || {}) as { id?: string };
|
||||
const promptId = typeof request.id === 'string' ? request.id : '';
|
||||
if (!promptId) {
|
||||
return { id, type, success: false, error: 'Prompt id is required' };
|
||||
}
|
||||
const data = await deps.resetMagicPromptOverride(promptId);
|
||||
return { id, type, success: true, data };
|
||||
}
|
||||
|
||||
case 'api:magic-prompts:reset-all': {
|
||||
const data = await deps.resetAllMagicPromptOverrides();
|
||||
return { id, type, success: true, data };
|
||||
}
|
||||
|
||||
case 'api:config/reload': {
|
||||
await ctx?.manager?.restart();
|
||||
return { id, type, success: true, data: { restarted: true } };
|
||||
|
||||
@@ -7,6 +7,11 @@ import type { BridgeContext } from './bridge';
|
||||
|
||||
const SETTINGS_KEY = 'openchamber.settings';
|
||||
const OPENCHAMBER_SHARED_SETTINGS_PATH = path.join(os.homedir(), '.config', 'openchamber', 'settings.json');
|
||||
const OPENCHAMBER_MAGIC_PROMPTS_PATH = path.join(os.homedir(), '.config', 'openchamber', 'magic-prompts.json');
|
||||
const MAGIC_PROMPTS_FILE_VERSION = 1;
|
||||
const MAGIC_PROMPT_ID_PATTERN = /^[a-z0-9._-]{1,160}$/;
|
||||
const MAGIC_PROMPT_TEXT_MAX_LENGTH = 200_000;
|
||||
const isVisiblePromptId = (id: string): boolean => id.endsWith('.visible');
|
||||
|
||||
const isPathInside = (candidatePath: string, parentPath: string): boolean => {
|
||||
const relative = path.relative(parentPath, candidatePath);
|
||||
@@ -166,6 +171,42 @@ const writeSharedSettingsToDisk = async (changes: Record<string, unknown>): Prom
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeMagicPromptOverrides = (input: unknown): Record<string, string> => {
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const next: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
||||
if (!MAGIC_PROMPT_ID_PATTERN.test(key) || typeof value !== 'string') {
|
||||
continue;
|
||||
}
|
||||
next[key] = value;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const readMagicPromptFile = (): { version: number; overrides: Record<string, string> } => {
|
||||
try {
|
||||
const raw = fs.readFileSync(OPENCHAMBER_MAGIC_PROMPTS_PATH, 'utf8');
|
||||
const parsed = JSON.parse(raw) as { overrides?: unknown };
|
||||
return {
|
||||
version: MAGIC_PROMPTS_FILE_VERSION,
|
||||
overrides: sanitizeMagicPromptOverrides(parsed?.overrides),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
version: MAGIC_PROMPTS_FILE_VERSION,
|
||||
overrides: {},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const writeMagicPromptFile = async (state: { version: number; overrides: Record<string, string> }): Promise<void> => {
|
||||
await fs.promises.mkdir(path.dirname(OPENCHAMBER_MAGIC_PROMPTS_PATH), { recursive: true });
|
||||
await fs.promises.writeFile(OPENCHAMBER_MAGIC_PROMPTS_PATH, JSON.stringify(state, null, 2), 'utf8');
|
||||
};
|
||||
|
||||
export const readSettings = (ctx?: BridgeContext): Record<string, unknown> => {
|
||||
const stored = ctx?.context?.globalState.get<Record<string, unknown>>(SETTINGS_KEY) || {};
|
||||
const restStored = { ...stored };
|
||||
@@ -229,3 +270,63 @@ export const persistSettings = async (changes: Record<string, unknown>, ctx?: Br
|
||||
|
||||
return merged;
|
||||
};
|
||||
|
||||
export const readMagicPromptOverrides = (): { version: number; overrides: Record<string, string> } => {
|
||||
return readMagicPromptFile();
|
||||
};
|
||||
|
||||
export const saveMagicPromptOverride = async (id: string, text: string): Promise<{ version: number; overrides: Record<string, string> }> => {
|
||||
const normalizedId = typeof id === 'string' ? id.trim() : '';
|
||||
if (!MAGIC_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 > MAGIC_PROMPT_TEXT_MAX_LENGTH) {
|
||||
throw new Error('Prompt text is too long');
|
||||
}
|
||||
|
||||
const current = readMagicPromptFile();
|
||||
const next = {
|
||||
version: MAGIC_PROMPTS_FILE_VERSION,
|
||||
overrides: {
|
||||
...current.overrides,
|
||||
[normalizedId]: text,
|
||||
},
|
||||
};
|
||||
await writeMagicPromptFile(next);
|
||||
return next;
|
||||
};
|
||||
|
||||
export const resetMagicPromptOverride = async (id: string): Promise<{ version: number; overrides: Record<string, string> }> => {
|
||||
const normalizedId = typeof id === 'string' ? id.trim() : '';
|
||||
if (!MAGIC_PROMPT_ID_PATTERN.test(normalizedId)) {
|
||||
throw new Error('Invalid prompt id');
|
||||
}
|
||||
|
||||
const current = readMagicPromptFile();
|
||||
if (!Object.prototype.hasOwnProperty.call(current.overrides, normalizedId)) {
|
||||
return current;
|
||||
}
|
||||
const nextOverrides = { ...current.overrides };
|
||||
delete nextOverrides[normalizedId];
|
||||
const next = {
|
||||
version: MAGIC_PROMPTS_FILE_VERSION,
|
||||
overrides: nextOverrides,
|
||||
};
|
||||
await writeMagicPromptFile(next);
|
||||
return next;
|
||||
};
|
||||
|
||||
export const resetAllMagicPromptOverrides = async (): Promise<{ version: number; overrides: Record<string, string> }> => {
|
||||
const next = {
|
||||
version: MAGIC_PROMPTS_FILE_VERSION,
|
||||
overrides: {},
|
||||
};
|
||||
await writeMagicPromptFile(next);
|
||||
return next;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,15 @@ import { handleFsBridgeMessage } from './bridge-fs-runtime';
|
||||
import { handleConfigBridgeMessage } from './bridge-config-runtime';
|
||||
import { handleSystemBridgeMessage } from './bridge-system-runtime';
|
||||
import { handleProxyBridgeMessage } from './bridge-proxy-runtime';
|
||||
import { fetchOpenCodeSkillsFromApi, persistSettings, readSettings } from './bridge-settings-runtime';
|
||||
import {
|
||||
fetchOpenCodeSkillsFromApi,
|
||||
persistSettings,
|
||||
readSettings,
|
||||
readMagicPromptOverrides,
|
||||
saveMagicPromptOverride,
|
||||
resetMagicPromptOverride,
|
||||
resetAllMagicPromptOverrides,
|
||||
} from './bridge-settings-runtime';
|
||||
import { execGit } from './bridge-git-process-runtime';
|
||||
import {
|
||||
parseDroppedFileReference,
|
||||
@@ -89,6 +97,10 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
{
|
||||
readSettings,
|
||||
persistSettings,
|
||||
readMagicPromptOverrides,
|
||||
saveMagicPromptOverride,
|
||||
resetMagicPromptOverride,
|
||||
resetAllMagicPromptOverrides,
|
||||
fetchOpenCodeSkillsFromApi,
|
||||
clientReloadDelayMs: CLIENT_RELOAD_DELAY_MS,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user