feat(ai-assist): pure prompt builder + result applier

buildAssistPrompt(action, selection) returns the {system, messages}
payload for rewrite/shorten/expand. Enforces 8KB selection cap and
typed errors with codes (unknown_action, bad_selection,
empty_selection, selection_too_large) so the IPC layer can map them
to user-friendly messages.

applyAssistResult(original, newText) trims the response and rejects
empty/unchanged payloads so a no-op streaming chunk doesn't churn
the editor.

15 new tests cover action coverage, prompt content, all four error
codes, cap-edge (exactly 8KB OK, 8KB+1 rejected), and trim semantics.

Amit Haridas
This commit is contained in:
Amit Haridas
2026-09-30 20:01:54 +05:30
parent b70dce9574
commit cb0b2eac3d
2 changed files with 235 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
/**
* Inline AI assist — prompt builder + result applier.
*
* Pure module. The actual network call goes through AiProviders.js; this
* module shapes the request (system + user messages) and validates the
* response before it reaches the editor.
*
* Actions (v4.13.0): rewrite / shorten / expand.
*
* Safety: MAX_SELECTION_CHARS caps the selection so a runaway or hostile
* caller can't push an 8KB+ blob at an API. The cap is enforced BEFORE
* the IPC hop so the main process sees only safe payloads.
*
* @module inline-assist
*/
const MAX_SELECTION_CHARS = 8 * 1024;
const ACTIONS = {
rewrite: {
label: 'Rewrite',
systemPrompt:
'You are a precise writing assistant. Rewrite the selected text to improve clarity, ' +
'flow, and word choice while preserving the original meaning, tone, and length. ' +
'Return ONLY the rewritten text — no preamble, no explanation, no surrounding quotes.',
},
shorten: {
label: 'Shorten',
systemPrompt:
'You are a precise writing assistant. Shorten the selected text while preserving all ' +
'key information. Aim for 30–50% reduction in length. Drop filler, redundancy, and ' +
'verbose phrasing. Return ONLY the shortened text — no preamble, no explanation.',
},
expand: {
label: 'Expand',
systemPrompt:
'You are a precise writing assistant. Expand the selected text by adding relevant ' +
'detail, examples, or explanation. Aim for 50–100% expansion. Stay on-topic. ' +
'Return ONLY the expanded text — no preamble, no explanation.',
},
};
const ACTIONS_LIST = Object.keys(ACTIONS);
/**
* Build the {system, messages} payload for `action`.
*
* @param {'rewrite'|'shorten'|'expand'} action
* @param {string} selection
* @returns {{system:string, messages:Array<{role:'user', content:string}>}}
* @throws when action is unknown or selection is empty / too large
*/
function buildAssistPrompt(action, selection) {
const def = ACTIONS[action];
if (!def) {
const err = new Error(`Unknown AI assist action: ${action}`);
err.code = 'unknown_action';
throw err;
}
if (typeof selection !== 'string') {
const err = new Error('Selection must be a string');
err.code = 'bad_selection';
throw err;
}
if (selection.length === 0) {
const err = new Error('Selection is empty');
err.code = 'empty_selection';
throw err;
}
if (selection.length > MAX_SELECTION_CHARS) {
const err = new Error(
`Selection is too large (${selection.length} chars; max ${MAX_SELECTION_CHARS})`
);
err.code = 'selection_too_large';
throw err;
}
return {
system: def.systemPrompt,
messages: [{ role: 'user', content: selection }],
};
}
/**
* Validate the provider's response before applying it to the editor.
*
* - Empty / whitespace-only → return null (don't replace selection with nothing)
* - Unchanged → return null (no-op, don't churn the editor)
*
* @param {string} originalText - The original selection
* @param {string} newText - The provider's response
* @returns {string|null} text to apply, or null to skip
*/
function applyAssistResult(originalText, newText) {
if (typeof newText !== 'string') return null;
const trimmed = newText.trim();
if (trimmed.length === 0) return null;
if (trimmed === String(originalText || '').trim()) return null;
return trimmed;
}
module.exports = {
buildAssistPrompt,
applyAssistResult,
ACTIONS,
ACTIONS_LIST,
MAX_SELECTION_CHARS,
};