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:
Bohdan Triapitsyn
2026-04-07 22:51:14 +03:00
committed by GitHub
parent 2e5b02e753
commit 5f0d1623ae
21 changed files with 1415 additions and 279 deletions
@@ -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;
};
+13 -1
View File
@@ -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,
},