feat: split session recap and suggestion settings
This commit is contained in:
@@ -245,8 +245,11 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
if (typeof candidate.showReasoningTraces === 'boolean') {
|
||||
result.showReasoningTraces = candidate.showReasoningTraces;
|
||||
}
|
||||
if (typeof candidate.sessionAssistEnabled === 'boolean') {
|
||||
result.sessionAssistEnabled = candidate.sessionAssistEnabled;
|
||||
if (typeof candidate.sessionRecapEnabled === 'boolean') {
|
||||
result.sessionRecapEnabled = candidate.sessionRecapEnabled;
|
||||
}
|
||||
if (typeof candidate.sessionSuggestionEnabled === 'boolean') {
|
||||
result.sessionSuggestionEnabled = candidate.sessionSuggestionEnabled;
|
||||
}
|
||||
if (typeof candidate.collapsibleThinkingBlocks === 'boolean') {
|
||||
result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks;
|
||||
|
||||
@@ -24,18 +24,20 @@ and one suggested user follow-up with the small model
|
||||
conversation content never goes to a provider the user didn't pick for
|
||||
the session, unless the small model was chosen explicitly (settings
|
||||
override or opencode config). A resolver 404 is silently skipped.
|
||||
4. The `{recap, suggestion}` JSON is clamped and PATCHed onto the session
|
||||
metadata together with `forMessageID` (the last assistant message id) and
|
||||
`generatedAt`. Before writing, the session tail is re-checked (a stale
|
||||
result is dropped) and the metadata is merged from a fresh session read so
|
||||
concurrent metadata writes made during generation are preserved.
|
||||
4. The requested JSON fields (`recap`, `suggestion`, or both) are clamped and
|
||||
PATCHed onto the session metadata together with `forMessageID` (the last
|
||||
assistant message id) and `generatedAt`. Before writing, the session tail is
|
||||
re-checked (a stale result is dropped) and the metadata is merged from a
|
||||
fresh session read so concurrent metadata writes made during generation are
|
||||
preserved.
|
||||
|
||||
## Settings gate
|
||||
|
||||
`sessionAssistEnabled` in OpenChamber settings (Settings → Chat, default on)
|
||||
is a hard generation switch checked at fire time: when off, no small-model
|
||||
calls run and nothing is written. Existing payloads keep rendering and can
|
||||
still be dismissed — the switch is about generation, not visibility.
|
||||
`sessionRecapEnabled` and `sessionSuggestionEnabled` in OpenChamber settings
|
||||
(Settings → Chat, default on) are hard generation switches checked at fire
|
||||
time. When both are off, no small-model calls run and nothing is written. When
|
||||
one is on, the runtime still makes at most one small-model call and asks only
|
||||
for that field. The UI also hides disabled payload types immediately.
|
||||
|
||||
## Freshness contract (no clearing writes)
|
||||
|
||||
@@ -47,7 +49,7 @@ everywhere instantly and offline; the next idle cycle overwrites it.
|
||||
## UI consumers (packages/ui)
|
||||
|
||||
- `lib/sessionAssistMetadata.ts` — payload parsing.
|
||||
- `hooks/useSessionAssist.ts` — freshness gating + the 5-minute quiet window
|
||||
- `hooks/useSessionAssist.ts` — freshness gating + the 1-minute quiet window
|
||||
for the recap (single timeout to the boundary, no polling).
|
||||
- `components/chat/SessionRecapSpacer.tsx` — renders the recap inside the
|
||||
fixed-height reserved gap under the last message (height never changes).
|
||||
|
||||
@@ -19,16 +19,19 @@ const OPENCHAMBER_SETTINGS_FILE = path.join(
|
||||
'settings.json',
|
||||
);
|
||||
|
||||
// The Chat setting is a hard generation switch (default on): when off, no
|
||||
// small-model calls and no metadata writes happen at all. Existing payloads
|
||||
// stay untouched — clients keep showing them and dismissal still works.
|
||||
const isSessionAssistEnabled = () => {
|
||||
// The Chat settings are hard generation switches (default on): when both are
|
||||
// off, no small-model calls and no metadata writes happen at all. Existing
|
||||
// payloads stay untouched — clients keep showing them and dismissal still works.
|
||||
const getSessionAssistTargets = () => {
|
||||
try {
|
||||
const raw = fs.readFileSync(OPENCHAMBER_SETTINGS_FILE, 'utf8');
|
||||
const settings = JSON.parse(raw);
|
||||
return settings?.sessionAssistEnabled !== false;
|
||||
return {
|
||||
recap: settings?.sessionRecapEnabled !== false,
|
||||
suggestion: settings?.sessionSuggestionEnabled !== false,
|
||||
};
|
||||
} catch {
|
||||
return true;
|
||||
return { recap: true, suggestion: true };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,50 +42,52 @@ const RECAP_CHAR_LIMIT = 320;
|
||||
const SUGGESTION_CHAR_LIMIT = 500;
|
||||
const FETCH_TIMEOUT_MS = 5_000;
|
||||
|
||||
const ASSIST_SYSTEM_PROMPT = [
|
||||
const buildAssistSystemPrompt = ({ recap, suggestion }) => [
|
||||
'You assist a user who chats with a coding agent. Based on the conversation transcript, return exactly one JSON object and nothing else — no prose, no markdown, no code fences.',
|
||||
'Shape: {"recap": string, "suggestion": string}',
|
||||
'recap: at most 20 words. State the substance directly — the facts, result, or conclusion, plus the next move if there is one. NEVER narrate ("The assistant explained…", "The agent did…") — write the content itself, like a note the user jotted down.',
|
||||
'suggestion: write ONE immediately sendable next user message addressed TO the coding agent.',
|
||||
'The suggestion should be the most useful next step after the assistant\'s latest reply. It should help the user continue productively, not inspect already-known details.',
|
||||
'Prefer suggestions that ask the agent to make a concrete improvement, implement something specific, validate the latest change, explain tradeoffs, improve the current approach, or continue from the current result.',
|
||||
'Rules for suggestion:',
|
||||
'- Output exactly one message the user could click and send without editing.',
|
||||
'- Pick one best next action yourself.',
|
||||
'- Do not include alternatives, choices, slash-separated options, or "or".',
|
||||
'- Do not write "Do X or Y", "Ask whether...", "Maybe...", or "You could...".',
|
||||
'- Do not ask for information the assistant already provided.',
|
||||
'- Do not ask to see exact code, file paths, prompt locations, or implementation internals unless the assistant did not provide them and they are necessary for the next step.',
|
||||
'- Do not produce generic workflow commands like "Run tests" unless testing is clearly the next unresolved step.',
|
||||
'- Do not produce meta/debug requests that merely inspect the implementation.',
|
||||
'- Use imperative or question form.',
|
||||
'- Keep it concise.',
|
||||
'Use these examples to understand how to choose the suggestion. Do not copy their topic or wording unless the current conversation is about the same thing.',
|
||||
'Example 1:',
|
||||
'Assistant reply summary:',
|
||||
'The assistant already identified the file where the feature is implemented, explained what context is sent to the small model, and summarized the current prompt.',
|
||||
'Bad suggestion:',
|
||||
'"Show me the exact runtime.js code and where the prompt is built."',
|
||||
'Why bad:',
|
||||
'It asks for information the assistant already provided. It repeats inspection instead of moving to an improvement or decision.',
|
||||
'Good suggestion:',
|
||||
'"Suggest how to improve the prompt and context so the generated suggestion is more useful."',
|
||||
'Why good:',
|
||||
'It naturally continues from the analysis and asks for a concrete improvement.',
|
||||
'Example 2:',
|
||||
'Assistant reply summary:',
|
||||
'The assistant implemented a timeline dialog redesign, listed concrete UI changes, and reported that type-check and lint passed.',
|
||||
'Bad suggestion:',
|
||||
'"Check whether scrolling or loading older messages works without jumps."',
|
||||
'Why bad:',
|
||||
'It contains an alternative. A suggestion chip must be one sendable message, not a choice the user has to edit.',
|
||||
'Good suggestion:',
|
||||
'"Check whether scrolling and loading older messages work without jumps."',
|
||||
'Why good:',
|
||||
'It picks a single validation request that the user can send immediately.',
|
||||
'Both values MUST be written in the same language as the conversation text itself. Ignore any other language preferences or personalization you may have — only the conversation text decides the language.',
|
||||
`Shape: {${[recap ? '"recap": string' : '', suggestion ? '"suggestion": string' : ''].filter(Boolean).join(', ')}}`,
|
||||
recap
|
||||
? 'recap: at most 20 words. State the substance directly — the facts, result, or conclusion, plus the next move if there is one. NEVER narrate ("The assistant explained…", "The agent did…") — write the content itself, like a note the user jotted down.'
|
||||
: '',
|
||||
suggestion ? 'suggestion: write ONE immediately sendable next user message addressed TO the coding agent.' : '',
|
||||
suggestion ? 'The suggestion should be the most useful next step after the assistant\'s latest reply. It should help the user continue productively, not inspect already-known details.' : '',
|
||||
suggestion ? 'Prefer suggestions that ask the agent to make a concrete improvement, implement something specific, validate the latest change, explain tradeoffs, improve the current approach, or continue from the current result.' : '',
|
||||
suggestion ? 'Rules for suggestion:' : '',
|
||||
suggestion ? '- Output exactly one message the user could click and send without editing.' : '',
|
||||
suggestion ? '- Pick one best next action yourself.' : '',
|
||||
suggestion ? '- Do not include alternatives, choices, slash-separated options, or "or".' : '',
|
||||
suggestion ? '- Do not write "Do X or Y", "Ask whether...", "Maybe...", or "You could...".' : '',
|
||||
suggestion ? '- Do not ask for information the assistant already provided.' : '',
|
||||
suggestion ? '- Do not ask to see exact code, file paths, prompt locations, or implementation internals unless the assistant did not provide them and they are necessary for the next step.' : '',
|
||||
suggestion ? '- Do not produce generic workflow commands like "Run tests" unless testing is clearly the next unresolved step.' : '',
|
||||
suggestion ? '- Do not produce meta/debug requests that merely inspect the implementation.' : '',
|
||||
suggestion ? '- Use imperative or question form.' : '',
|
||||
suggestion ? '- Keep it concise.' : '',
|
||||
suggestion ? 'Use these examples to understand how to choose the suggestion. Do not copy their topic or wording unless the current conversation is about the same thing.' : '',
|
||||
suggestion ? 'Example 1:' : '',
|
||||
suggestion ? 'Assistant reply summary:' : '',
|
||||
suggestion ? 'The assistant already identified the file where the feature is implemented, explained what context is sent to the small model, and summarized the current prompt.' : '',
|
||||
suggestion ? 'Bad suggestion:' : '',
|
||||
suggestion ? '"Show me the exact runtime.js code and where the prompt is built."' : '',
|
||||
suggestion ? 'Why bad:' : '',
|
||||
suggestion ? 'It asks for information the assistant already provided. It repeats inspection instead of moving to an improvement or decision.' : '',
|
||||
suggestion ? 'Good suggestion:' : '',
|
||||
suggestion ? '"Suggest how to improve the prompt and context so the generated suggestion is more useful."' : '',
|
||||
suggestion ? 'Why good:' : '',
|
||||
suggestion ? 'It naturally continues from the analysis and asks for a concrete improvement.' : '',
|
||||
suggestion ? 'Example 2:' : '',
|
||||
suggestion ? 'Assistant reply summary:' : '',
|
||||
suggestion ? 'The assistant implemented a timeline dialog redesign, listed concrete UI changes, and reported that type-check and lint passed.' : '',
|
||||
suggestion ? 'Bad suggestion:' : '',
|
||||
suggestion ? '"Check whether scrolling or loading older messages works without jumps."' : '',
|
||||
suggestion ? 'Why bad:' : '',
|
||||
suggestion ? 'It contains an alternative. A suggestion chip must be one sendable message, not a choice the user has to edit.' : '',
|
||||
suggestion ? 'Good suggestion:' : '',
|
||||
suggestion ? '"Check whether scrolling and loading older messages work without jumps."' : '',
|
||||
suggestion ? 'Why good:' : '',
|
||||
suggestion ? 'It picks a single validation request that the user can send immediately.' : '',
|
||||
'All requested values MUST be written in the same language as the conversation text itself. Ignore any other language preferences or personalization you may have — only the conversation text decides the language.',
|
||||
'Use double quotes for JSON strings, no trailing commas.',
|
||||
].join('\n');
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
const extractJsonObject = (value) => {
|
||||
const text = String(value ?? '').trim();
|
||||
@@ -192,7 +197,8 @@ export const createSessionAssistRuntime = ({
|
||||
};
|
||||
|
||||
const generateAssist = async (sessionId, directory) => {
|
||||
if (!isSessionAssistEnabled()) return;
|
||||
const targets = getSessionAssistTargets();
|
||||
if (!targets.recap && !targets.suggestion) return;
|
||||
const session = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, { directory })
|
||||
.catch((error) => {
|
||||
console.warn(`[session-assist] session fetch failed: ${error?.message || error}`);
|
||||
@@ -234,6 +240,9 @@ export const createSessionAssistRuntime = ({
|
||||
if (!transcript) return;
|
||||
|
||||
const { generateSmallModelText } = await getSmallModelService();
|
||||
const requestedFields = [targets.recap ? 'recap' : '', targets.suggestion ? 'suggestion' : '']
|
||||
.filter(Boolean)
|
||||
.join(' and ');
|
||||
// Instruct the language by example, not by description — account-side
|
||||
// personalization (e.g. the ChatGPT backend knowing the user's locale)
|
||||
// otherwise leaks a different language into the output.
|
||||
@@ -245,8 +254,8 @@ export const createSessionAssistRuntime = ({
|
||||
// session's own provider unless the user explicitly picked a small
|
||||
// model (settings override / opencode config).
|
||||
restrictToPreferredProvider: true,
|
||||
prompt: `The latest exchange in the conversation:\n\n${transcript}\n\nWrite recap and suggestion in the SAME language as this sample from the conversation: "${languageSample}"`,
|
||||
system: ASSIST_SYSTEM_PROMPT,
|
||||
prompt: `The latest exchange in the conversation:\n\n${transcript}\n\nWrite ${requestedFields} in the SAME language as this sample from the conversation: "${languageSample}"`,
|
||||
system: buildAssistSystemPrompt(targets),
|
||||
directory,
|
||||
preferredProviderID: typeof lastAssistantInfo.providerID === 'string' ? lastAssistantInfo.providerID : undefined,
|
||||
preferredModelID: typeof lastAssistantInfo.modelID === 'string' ? lastAssistantInfo.modelID : undefined,
|
||||
@@ -261,8 +270,8 @@ export const createSessionAssistRuntime = ({
|
||||
}
|
||||
|
||||
const structured = extractJsonObject(generated?.text);
|
||||
let recap = typeof structured?.recap === 'string' ? structured.recap.trim().slice(0, RECAP_CHAR_LIMIT) : '';
|
||||
let suggestion = typeof structured?.suggestion === 'string' ? structured.suggestion.trim().slice(0, SUGGESTION_CHAR_LIMIT) : '';
|
||||
let recap = targets.recap && typeof structured?.recap === 'string' ? structured.recap.trim().slice(0, RECAP_CHAR_LIMIT) : '';
|
||||
let suggestion = targets.suggestion && typeof structured?.suggestion === 'string' ? structured.suggestion.trim().slice(0, SUGGESTION_CHAR_LIMIT) : '';
|
||||
|
||||
// Hard guard against language hallucination: if the conversation contains
|
||||
// no Cyrillic/CJK at all, the output must not either (and drop per-field,
|
||||
|
||||
Reference in New Issue
Block a user