feat: split session recap and suggestion settings

This commit is contained in:
Bohdan Triapitsyn
2026-07-06 22:54:31 +03:00
parent 1824b51155
commit 182fc89b8d
22 changed files with 214 additions and 121 deletions
@@ -10,7 +10,7 @@ interface SessionRecapNoteProps {
// Quiet one-paragraph recap of the agent's last reply, rendered right under // Quiet one-paragraph recap of the agent's last reply, rendered right under
// the last message (above the reserved bottom gap). Appears only after the // the last message (above the reserved bottom gap). Appears only after the
// 5-minute quiet window, so the layout shift happens off-screen in practice. // 1-minute quiet window, so the layout shift happens off-screen in practice.
export const SessionRecapNote: React.FC<SessionRecapNoteProps> = React.memo(({ sessionId, directory, isMobile }) => { export const SessionRecapNote: React.FC<SessionRecapNoteProps> = React.memo(({ sessionId, directory, isMobile }) => {
const { visibleRecap } = useSessionAssistState(sessionId, directory); const { visibleRecap } = useSessionAssistState(sessionId, directory);
const { t } = useI18n(); const { t } = useI18n();
@@ -259,8 +259,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
const { browserTab } = usePwaDetection(); const { browserTab } = usePwaDetection();
const directoryShowHidden = useDirectoryShowHidden(); const directoryShowHidden = useDirectoryShowHidden();
const showReasoningTraces = useUIStore(state => state.showReasoningTraces); const showReasoningTraces = useUIStore(state => state.showReasoningTraces);
const sessionAssistEnabled = useUIStore(state => state.sessionAssistEnabled); const sessionRecapEnabled = useUIStore(state => state.sessionRecapEnabled);
const setSessionAssistEnabled = useUIStore(state => state.setSessionAssistEnabled); const sessionSuggestionEnabled = useUIStore(state => state.sessionSuggestionEnabled);
const setSessionRecapEnabled = useUIStore(state => state.setSessionRecapEnabled);
const setSessionSuggestionEnabled = useUIStore(state => state.setSessionSuggestionEnabled);
const setShowReasoningTraces = useUIStore(state => state.setShowReasoningTraces); const setShowReasoningTraces = useUIStore(state => state.setShowReasoningTraces);
const collapsibleThinkingBlocks = useUIStore(state => state.collapsibleThinkingBlocks); const collapsibleThinkingBlocks = useUIStore(state => state.collapsibleThinkingBlocks);
const setCollapsibleThinkingBlocks = useUIStore(state => state.setCollapsibleThinkingBlocks); const setCollapsibleThinkingBlocks = useUIStore(state => state.setCollapsibleThinkingBlocks);
@@ -1780,27 +1782,50 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
{(shouldShow('sessionAssist') || shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && ( {(shouldShow('sessionAssist') || shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && (
<section className="p-2 space-y-0.5"> <section className="p-2 space-y-0.5">
{shouldShow('sessionAssist') && ( {shouldShow('sessionAssist') && (
<>
<div <div
data-settings-item="chat.session-assist" data-settings-item="chat.session-recap"
className="group flex cursor-pointer items-center gap-2 py-0.5" className="group flex cursor-pointer items-center gap-2 py-0.5"
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-pressed={sessionAssistEnabled} aria-pressed={sessionRecapEnabled}
onClick={() => setSessionAssistEnabled(!sessionAssistEnabled)} onClick={() => setSessionRecapEnabled(!sessionRecapEnabled)}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === ' ' || event.key === 'Enter') { if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
setSessionAssistEnabled(!sessionAssistEnabled); setSessionRecapEnabled(!sessionRecapEnabled);
} }
}} }}
> >
<Checkbox <Checkbox
checked={sessionAssistEnabled} checked={sessionRecapEnabled}
onChange={setSessionAssistEnabled} onChange={setSessionRecapEnabled}
ariaLabel={t('settings.openchamber.visual.field.sessionAssistAria')} ariaLabel={t('settings.openchamber.visual.field.sessionRecapAria')}
/> />
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.sessionAssist')}</span> <span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.sessionRecap')}</span>
</div> </div>
<div
data-settings-item="chat.session-suggestion"
className="group flex cursor-pointer items-center gap-2 py-0.5"
role="button"
tabIndex={0}
aria-pressed={sessionSuggestionEnabled}
onClick={() => setSessionSuggestionEnabled(!sessionSuggestionEnabled)}
onKeyDown={(event) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
setSessionSuggestionEnabled(!sessionSuggestionEnabled);
}
}}
>
<Checkbox
checked={sessionSuggestionEnabled}
onChange={setSessionSuggestionEnabled}
ariaLabel={t('settings.openchamber.visual.field.sessionSuggestionAria')}
/>
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.sessionSuggestion')}</span>
</div>
</>
)} )}
{shouldShow('reasoning') && ( {shouldShow('reasoning') && (
<div <div
+7 -4
View File
@@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { useDirectoryStore, useSession, useSessionStatus } from '@/sync/sync-context'; import { useDirectoryStore, useSession, useSessionStatus } from '@/sync/sync-context';
import { getSessionAssist, type SessionAssistPayload } from '@/lib/sessionAssistMetadata'; import { getSessionAssist, type SessionAssistPayload } from '@/lib/sessionAssistMetadata';
import { useUIStore } from '@/stores/useUIStore';
// How long the chat must sit untouched before the recap becomes visible. // How long the chat must sit untouched before the recap becomes visible.
// The suggestion has no such delay — it shows as soon as it arrives. // The suggestion has no such delay — it shows as soon as it arrives.
export const RECAP_VISIBILITY_DELAY_MS = 5 * 60 * 1000; export const RECAP_VISIBILITY_DELAY_MS = 60 * 1000;
interface LastMessageSnapshot { interface LastMessageSnapshot {
id: string; id: string;
@@ -50,7 +51,7 @@ function useLastMessageSnapshot(sessionId: string, directory?: string): LastMess
export interface SessionAssistState { export interface SessionAssistState {
/** Valid (fresh) assist payload, or null. */ /** Valid (fresh) assist payload, or null. */
assist: SessionAssistPayload | null; assist: SessionAssistPayload | null;
/** Recap text, only when the 5-minute quiet window has elapsed. */ /** Recap text, only when the 1-minute quiet window has elapsed. */
visibleRecap: string | null; visibleRecap: string | null;
/** Suggestion text — fresh payload, session idle; caller still gates on input emptiness. */ /** Suggestion text — fresh payload, session idle; caller still gates on input emptiness. */
suggestion: string | null; suggestion: string | null;
@@ -60,6 +61,8 @@ export function useSessionAssistState(sessionId: string, directory?: string): Se
const session = useSession(sessionId, directory); const session = useSession(sessionId, directory);
const status = useSessionStatus(sessionId, directory); const status = useSessionStatus(sessionId, directory);
const lastMessage = useLastMessageSnapshot(sessionId, directory); const lastMessage = useLastMessageSnapshot(sessionId, directory);
const sessionRecapEnabled = useUIStore((state) => state.sessionRecapEnabled);
const sessionSuggestionEnabled = useUIStore((state) => state.sessionSuggestionEnabled);
const isIdle = !status || status.type === 'idle'; const isIdle = !status || status.type === 'idle';
const payload = getSessionAssist(session); const payload = getSessionAssist(session);
@@ -88,7 +91,7 @@ export function useSessionAssistState(sessionId: string, directory?: string): Se
return { return {
assist, assist,
visibleRecap: assist && assist.recap && quietElapsed ? assist.recap : null, visibleRecap: sessionRecapEnabled && assist && assist.recap && quietElapsed ? assist.recap : null,
suggestion: assist && assist.suggestion ? assist.suggestion : null, suggestion: sessionSuggestionEnabled && assist && assist.suggestion ? assist.suggestion : null,
}; };
} }
+11 -5
View File
@@ -6,7 +6,8 @@ import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode';
type AppearanceSlice = { type AppearanceSlice = {
showReasoningTraces: boolean; showReasoningTraces: boolean;
sessionAssistEnabled: boolean; sessionRecapEnabled: boolean;
sessionSuggestionEnabled: boolean;
collapsibleThinkingBlocks: boolean; collapsibleThinkingBlocks: boolean;
showDeletionDialog: boolean; showDeletionDialog: boolean;
nativeNotificationsEnabled: boolean; nativeNotificationsEnabled: boolean;
@@ -51,7 +52,8 @@ export const startAppearanceAutoSave = (): void => {
let previous: AppearanceSlice = { let previous: AppearanceSlice = {
showReasoningTraces: useUIStore.getState().showReasoningTraces, showReasoningTraces: useUIStore.getState().showReasoningTraces,
sessionAssistEnabled: useUIStore.getState().sessionAssistEnabled, sessionRecapEnabled: useUIStore.getState().sessionRecapEnabled,
sessionSuggestionEnabled: useUIStore.getState().sessionSuggestionEnabled,
collapsibleThinkingBlocks: useUIStore.getState().collapsibleThinkingBlocks, collapsibleThinkingBlocks: useUIStore.getState().collapsibleThinkingBlocks,
showDeletionDialog: useUIStore.getState().showDeletionDialog, showDeletionDialog: useUIStore.getState().showDeletionDialog,
nativeNotificationsEnabled: useUIStore.getState().nativeNotificationsEnabled, nativeNotificationsEnabled: useUIStore.getState().nativeNotificationsEnabled,
@@ -103,7 +105,8 @@ export const startAppearanceAutoSave = (): void => {
useUIStore.subscribe((state) => { useUIStore.subscribe((state) => {
const current: AppearanceSlice = { const current: AppearanceSlice = {
showReasoningTraces: state.showReasoningTraces, showReasoningTraces: state.showReasoningTraces,
sessionAssistEnabled: state.sessionAssistEnabled, sessionRecapEnabled: state.sessionRecapEnabled,
sessionSuggestionEnabled: state.sessionSuggestionEnabled,
collapsibleThinkingBlocks: state.collapsibleThinkingBlocks, collapsibleThinkingBlocks: state.collapsibleThinkingBlocks,
showDeletionDialog: state.showDeletionDialog, showDeletionDialog: state.showDeletionDialog,
nativeNotificationsEnabled: state.nativeNotificationsEnabled, nativeNotificationsEnabled: state.nativeNotificationsEnabled,
@@ -137,8 +140,11 @@ export const startAppearanceAutoSave = (): void => {
if (current.showReasoningTraces !== previous.showReasoningTraces) { if (current.showReasoningTraces !== previous.showReasoningTraces) {
diff.showReasoningTraces = current.showReasoningTraces; diff.showReasoningTraces = current.showReasoningTraces;
} }
if (current.sessionAssistEnabled !== previous.sessionAssistEnabled) { if (current.sessionRecapEnabled !== previous.sessionRecapEnabled) {
diff.sessionAssistEnabled = current.sessionAssistEnabled; diff.sessionRecapEnabled = current.sessionRecapEnabled;
}
if (current.sessionSuggestionEnabled !== previous.sessionSuggestionEnabled) {
diff.sessionSuggestionEnabled = current.sessionSuggestionEnabled;
} }
if (current.collapsibleThinkingBlocks !== previous.collapsibleThinkingBlocks) { if (current.collapsibleThinkingBlocks !== previous.collapsibleThinkingBlocks) {
diff.collapsibleThinkingBlocks = current.collapsibleThinkingBlocks; diff.collapsibleThinkingBlocks = current.collapsibleThinkingBlocks;
+2 -1
View File
@@ -114,7 +114,8 @@ export type DesktopSettings = {
defaultVariant?: string; defaultVariant?: string;
defaultAgent?: string; defaultAgent?: string;
smallModelUseDefault?: boolean; smallModelUseDefault?: boolean;
sessionAssistEnabled?: boolean; sessionRecapEnabled?: boolean;
sessionSuggestionEnabled?: boolean;
smallModelOverride?: string; // format: "provider/model" smallModelOverride?: string; // format: "provider/model"
defaultGitIdentityId?: string; // ''/undefined = unset, 'global' or profile id defaultGitIdentityId?: string; // ''/undefined = unset, 'global' or profile id
openInAppId?: string; openInAppId?: string;
@@ -1690,8 +1690,10 @@ export const settingsDict = {
'settings.openchamber.visual.field.userMessageRenderingAria': 'User message rendering: {option}', 'settings.openchamber.visual.field.userMessageRenderingAria': 'User message rendering: {option}',
'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid rendering: {option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid rendering: {option}',
'settings.openchamber.visual.field.diffLayoutAria': 'Diff layout: {option}', 'settings.openchamber.visual.field.diffLayoutAria': 'Diff layout: {option}',
'settings.openchamber.visual.field.sessionAssist': 'Generate Session Recap & Suggestion', 'settings.openchamber.visual.field.sessionRecap': 'Generate Session Recap',
'settings.openchamber.visual.field.sessionAssistAria': 'Generate a recap and a suggested reply after the agent finishes', 'settings.openchamber.visual.field.sessionRecapAria': 'Generate a recap after the agent finishes',
'settings.openchamber.visual.field.sessionSuggestion': 'Generate Next User Message Suggestion',
'settings.openchamber.visual.field.sessionSuggestionAria': 'Generate a suggested next user message after the agent finishes',
'settings.openchamber.visual.field.showReasoningTracesAria': 'Show reasoning traces', 'settings.openchamber.visual.field.showReasoningTracesAria': 'Show reasoning traces',
'settings.openchamber.visual.field.showReasoningTraces': 'Show Reasoning Traces', 'settings.openchamber.visual.field.showReasoningTraces': 'Show Reasoning Traces',
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Enable collapsible reasoning blocks', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Enable collapsible reasoning blocks',
@@ -1657,8 +1657,10 @@ export const settingsDict = {
"settings.openchamber.visual.field.userMessageRenderingAria": "Renderizado de mensajes del usuario: {option}", "settings.openchamber.visual.field.userMessageRenderingAria": "Renderizado de mensajes del usuario: {option}",
"settings.openchamber.visual.field.mermaidRenderingAria": "Renderizado de Mermaid: {option}", "settings.openchamber.visual.field.mermaidRenderingAria": "Renderizado de Mermaid: {option}",
"settings.openchamber.visual.field.diffLayoutAria": "Diseño de comparación: {option}", "settings.openchamber.visual.field.diffLayoutAria": "Diseño de comparación: {option}",
"settings.openchamber.visual.field.sessionAssist": "Generar resumen y sugerencia de sesión", "settings.openchamber.visual.field.sessionRecap": "Generar resumen de sesión",
"settings.openchamber.visual.field.sessionAssistAria": "Generar un resumen y una respuesta sugerida cuando el agente termina", "settings.openchamber.visual.field.sessionRecapAria": "Generar un resumen cuando el agente termina",
"settings.openchamber.visual.field.sessionSuggestion": "Generar sugerencia del próximo mensaje del usuario",
"settings.openchamber.visual.field.sessionSuggestionAria": "Generar un próximo mensaje sugerido del usuario cuando el agente termina",
"settings.openchamber.visual.field.showReasoningTracesAria": "Mostrar rastros de razonamiento", "settings.openchamber.visual.field.showReasoningTracesAria": "Mostrar rastros de razonamiento",
"settings.openchamber.visual.field.showReasoningTraces": "Mostrar trazas de razonamiento", "settings.openchamber.visual.field.showReasoningTraces": "Mostrar trazas de razonamiento",
"settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar bloques de razonamiento colapsables", "settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar bloques de razonamiento colapsables",
@@ -1624,8 +1624,10 @@ export const settingsDict = {
'settings.openchamber.visual.field.userMessageRenderingAria': 'Rendu du message utilisateur : {option}', 'settings.openchamber.visual.field.userMessageRenderingAria': 'Rendu du message utilisateur : {option}',
'settings.openchamber.visual.field.mermaidRenderingAria': 'Rendu Mermaid : {option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Rendu Mermaid : {option}',
'settings.openchamber.visual.field.diffLayoutAria': 'Disposition des différences : {option}', 'settings.openchamber.visual.field.diffLayoutAria': 'Disposition des différences : {option}',
'settings.openchamber.visual.field.sessionAssist': 'Générer le récapitulatif et la suggestion de session', 'settings.openchamber.visual.field.sessionRecap': 'Générer le récapitulatif de session',
'settings.openchamber.visual.field.sessionAssistAria': "Générer un récapitulatif et une réponse suggérée quand l'agent termine", 'settings.openchamber.visual.field.sessionRecapAria': "Générer un récapitulatif quand l'agent termine",
'settings.openchamber.visual.field.sessionSuggestion': 'Générer une suggestion de prochain message utilisateur',
'settings.openchamber.visual.field.sessionSuggestionAria': "Générer un prochain message utilisateur suggéré quand l'agent termine",
'settings.openchamber.visual.field.showReasoningTracesAria': 'Afficher les traces de raisonnement', 'settings.openchamber.visual.field.showReasoningTracesAria': 'Afficher les traces de raisonnement',
'settings.openchamber.visual.field.showReasoningTraces': 'Afficher les traces de raisonnement', 'settings.openchamber.visual.field.showReasoningTraces': 'Afficher les traces de raisonnement',
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Activer les blocs de raisonnement pliables', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Activer les blocs de raisonnement pliables',
@@ -1690,8 +1690,10 @@ export const settingsDict = {
'settings.openchamber.visual.field.userMessageRenderingAria': 'ユーザーメッセージ表示: {option}', 'settings.openchamber.visual.field.userMessageRenderingAria': 'ユーザーメッセージ表示: {option}',
'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 表示: {option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 表示: {option}',
'settings.openchamber.visual.field.diffLayoutAria': 'Diff レイアウト: {option}', 'settings.openchamber.visual.field.diffLayoutAria': 'Diff レイアウト: {option}',
'settings.openchamber.visual.field.sessionAssist': 'セッションの要約と提案を生成', 'settings.openchamber.visual.field.sessionRecap': 'セッションの要約を生成',
'settings.openchamber.visual.field.sessionAssistAria': 'エージェントの完了後に要約と返信の提案を生成します', 'settings.openchamber.visual.field.sessionRecapAria': 'エージェントの完了後に要約を生成します',
'settings.openchamber.visual.field.sessionSuggestion': '次のユーザーメッセージの提案を生成',
'settings.openchamber.visual.field.sessionSuggestionAria': 'エージェントの完了後に次のユーザーメッセージの提案を生成します',
'settings.openchamber.visual.field.showReasoningTracesAria': '推論トレースを表示', 'settings.openchamber.visual.field.showReasoningTracesAria': '推論トレースを表示',
'settings.openchamber.visual.field.showReasoningTraces': '推論トレースを表示', 'settings.openchamber.visual.field.showReasoningTraces': '推論トレースを表示',
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '折りたたみ可能な推論ブロックを有効化', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '折りたたみ可能な推論ブロックを有効化',
@@ -1657,8 +1657,10 @@ export const settingsDict = {
'settings.openchamber.visual.field.userMessageRenderingAria': '사용자 메시지 렌더링: {option}', 'settings.openchamber.visual.field.userMessageRenderingAria': '사용자 메시지 렌더링: {option}',
'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 렌더링: {option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 렌더링: {option}',
'settings.openchamber.visual.field.diffLayoutAria': 'Diff 레이아웃: {option}', 'settings.openchamber.visual.field.diffLayoutAria': 'Diff 레이아웃: {option}',
'settings.openchamber.visual.field.sessionAssist': '세션 요약 및 제안 생성', 'settings.openchamber.visual.field.sessionRecap': '세션 요약 생성',
'settings.openchamber.visual.field.sessionAssistAria': '에이전트가 완료되면 요약과 제안 답장을 생성합니다', 'settings.openchamber.visual.field.sessionRecapAria': '에이전트가 완료되면 요약을 생성합니다',
'settings.openchamber.visual.field.sessionSuggestion': '다음 사용자 메시지 제안 생성',
'settings.openchamber.visual.field.sessionSuggestionAria': '에이전트가 완료되면 다음 사용자 메시지 제안을 생성합니다',
'settings.openchamber.visual.field.showReasoningTracesAria': 'Reasoning trace 표시', 'settings.openchamber.visual.field.showReasoningTracesAria': 'Reasoning trace 표시',
'settings.openchamber.visual.field.showReasoningTraces': 'Reasoning Trace 표시', 'settings.openchamber.visual.field.showReasoningTraces': 'Reasoning Trace 표시',
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '접을 수 있는 추론 블록 활성화', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '접을 수 있는 추론 블록 활성화',
@@ -974,8 +974,10 @@ export const settingsDict = {
'settings.openchamber.visual.field.showDotfilesAria': 'Pokaż pliki ukryte', 'settings.openchamber.visual.field.showDotfilesAria': 'Pokaż pliki ukryte',
'settings.openchamber.visual.field.showExpandedBashToolsAria': 'Pokaż rozwinięte narzędzia bash', 'settings.openchamber.visual.field.showExpandedBashToolsAria': 'Pokaż rozwinięte narzędzia bash',
'settings.openchamber.visual.field.showExpandedEditToolsAria': 'Pokaż rozwinięte narzędzia edycji', 'settings.openchamber.visual.field.showExpandedEditToolsAria': 'Pokaż rozwinięte narzędzia edycji',
'settings.openchamber.visual.field.sessionAssist': 'Generuj podsumowanie i sugestię sesji', 'settings.openchamber.visual.field.sessionRecap': 'Generuj podsumowanie sesji',
'settings.openchamber.visual.field.sessionAssistAria': 'Generuj podsumowanie i sugerowaną odpowiedź po zakończeniu pracy agenta', 'settings.openchamber.visual.field.sessionRecapAria': 'Generuj podsumowanie po zakończeniu pracy agenta',
'settings.openchamber.visual.field.sessionSuggestion': 'Generuj sugestię następnej wiadomości użytkownika',
'settings.openchamber.visual.field.sessionSuggestionAria': 'Generuj sugerowaną następną wiadomość użytkownika po zakończeniu pracy agenta',
'settings.openchamber.visual.field.showReasoningTraces': 'Pokaż ślady rozumowania', 'settings.openchamber.visual.field.showReasoningTraces': 'Pokaż ślady rozumowania',
'settings.openchamber.visual.field.showReasoningTracesAria': 'Pokaż ślady rozumowania', 'settings.openchamber.visual.field.showReasoningTracesAria': 'Pokaż ślady rozumowania',
'settings.openchamber.visual.field.collapsibleThinkingBlocks': 'Włącz zwijalne bloki rozumowania', 'settings.openchamber.visual.field.collapsibleThinkingBlocks': 'Włącz zwijalne bloki rozumowania',
@@ -1657,8 +1657,10 @@ export const settingsDict = {
"settings.openchamber.visual.field.userMessageRenderingAria": "Renderizado de mensagens do usuário: {option}", "settings.openchamber.visual.field.userMessageRenderingAria": "Renderizado de mensagens do usuário: {option}",
"settings.openchamber.visual.field.mermaidRenderingAria": "Renderizado de Mermaid: {option}", "settings.openchamber.visual.field.mermaidRenderingAria": "Renderizado de Mermaid: {option}",
"settings.openchamber.visual.field.diffLayoutAria": "Layout de comparação: {option}", "settings.openchamber.visual.field.diffLayoutAria": "Layout de comparação: {option}",
"settings.openchamber.visual.field.sessionAssist": "Gerar resumo e sugestão da sessão", "settings.openchamber.visual.field.sessionRecap": "Gerar resumo da sessão",
"settings.openchamber.visual.field.sessionAssistAria": "Gerar um resumo e uma resposta sugerida quando o agente termina", "settings.openchamber.visual.field.sessionRecapAria": "Gerar um resumo quando o agente termina",
"settings.openchamber.visual.field.sessionSuggestion": "Gerar sugestão da próxima mensagem do usuário",
"settings.openchamber.visual.field.sessionSuggestionAria": "Gerar uma próxima mensagem sugerida do usuário quando o agente termina",
"settings.openchamber.visual.field.showReasoningTracesAria": "Mostrar rastros de raciocínio", "settings.openchamber.visual.field.showReasoningTracesAria": "Mostrar rastros de raciocínio",
"settings.openchamber.visual.field.showReasoningTraces": "Mostrar rastros de raciocínio", "settings.openchamber.visual.field.showReasoningTraces": "Mostrar rastros de raciocínio",
"settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar blocos de raciocínio recolhíveis", "settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar blocos de raciocínio recolhíveis",
@@ -1657,8 +1657,10 @@ export const settingsDict = {
"settings.openchamber.visual.field.userMessageRenderingAria": "Відображення повідомлень користувача: {option}", "settings.openchamber.visual.field.userMessageRenderingAria": "Відображення повідомлень користувача: {option}",
"settings.openchamber.visual.field.mermaidRenderingAria": "Візуалізація Mermaid: {option}", "settings.openchamber.visual.field.mermaidRenderingAria": "Візуалізація Mermaid: {option}",
"settings.openchamber.visual.field.diffLayoutAria": "Компонування diff: {option}", "settings.openchamber.visual.field.diffLayoutAria": "Компонування diff: {option}",
"settings.openchamber.visual.field.sessionAssist": "Генерувати підсумок і пропозицію для сесії", "settings.openchamber.visual.field.sessionRecap": "Генерувати підсумок сесії",
"settings.openchamber.visual.field.sessionAssistAria": "Генерувати підсумок і запропоновану відповідь після завершення роботи агента", "settings.openchamber.visual.field.sessionRecapAria": "Генерувати підсумок після завершення роботи агента",
"settings.openchamber.visual.field.sessionSuggestion": "Генерувати пропозицію наступного повідомлення користувача",
"settings.openchamber.visual.field.sessionSuggestionAria": "Генерувати запропоноване наступне повідомлення користувача після завершення роботи агента",
"settings.openchamber.visual.field.showReasoningTracesAria": "Показати сліди міркувань", "settings.openchamber.visual.field.showReasoningTracesAria": "Показати сліди міркувань",
"settings.openchamber.visual.field.showReasoningTraces": "Показати сліди міркувань", "settings.openchamber.visual.field.showReasoningTraces": "Показати сліди міркувань",
"settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Увімкнути згортальні блоки міркувань", "settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Увімкнути згортальні блоки міркувань",
@@ -1657,8 +1657,10 @@ export const settingsDict = {
'settings.openchamber.visual.field.userMessageRenderingAria': '用户消息渲染:{option}', 'settings.openchamber.visual.field.userMessageRenderingAria': '用户消息渲染:{option}',
'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 渲染:{option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 渲染:{option}',
'settings.openchamber.visual.field.diffLayoutAria': '差异布局:{option}', 'settings.openchamber.visual.field.diffLayoutAria': '差异布局:{option}',
'settings.openchamber.visual.field.sessionAssist': '生成会话回顾与建议', 'settings.openchamber.visual.field.sessionRecap': '生成会话回顾',
'settings.openchamber.visual.field.sessionAssistAria': '代理完成后生成回顾和建议回复', 'settings.openchamber.visual.field.sessionRecapAria': '代理完成后生成回顾',
'settings.openchamber.visual.field.sessionSuggestion': '生成下一条用户消息建议',
'settings.openchamber.visual.field.sessionSuggestionAria': '代理完成后生成下一条用户消息建议',
'settings.openchamber.visual.field.showReasoningTracesAria': '显示推理轨迹', 'settings.openchamber.visual.field.showReasoningTracesAria': '显示推理轨迹',
'settings.openchamber.visual.field.showReasoningTraces': '显示推理轨迹', 'settings.openchamber.visual.field.showReasoningTraces': '显示推理轨迹',
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '启用可折叠推理块', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '启用可折叠推理块',
@@ -1573,8 +1573,10 @@
'settings.openchamber.visual.field.userMessageRenderingAria': '使用者訊息渲染:{option}', 'settings.openchamber.visual.field.userMessageRenderingAria': '使用者訊息渲染:{option}',
'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 渲染:{option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 渲染:{option}',
'settings.openchamber.visual.field.diffLayoutAria': '差異佈局:{option}', 'settings.openchamber.visual.field.diffLayoutAria': '差異佈局:{option}',
'settings.openchamber.visual.field.sessionAssist': '產生工作階段回顧與建議', 'settings.openchamber.visual.field.sessionRecap': '產生工作階段回顧',
'settings.openchamber.visual.field.sessionAssistAria': '代理完成後產生回顧與建議回覆', 'settings.openchamber.visual.field.sessionRecapAria': '代理完成後產生回顧',
'settings.openchamber.visual.field.sessionSuggestion': '產生下一則使用者訊息建議',
'settings.openchamber.visual.field.sessionSuggestionAria': '代理完成後產生下一則使用者訊息建議',
'settings.openchamber.visual.field.showReasoningTracesAria': '顯示推理軌跡', 'settings.openchamber.visual.field.showReasoningTracesAria': '顯示推理軌跡',
'settings.openchamber.visual.field.showReasoningTraces': '顯示推理軌跡', 'settings.openchamber.visual.field.showReasoningTraces': '顯示推理軌跡',
'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '啟用可摺疊推理區塊', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '啟用可摺疊推理區塊',
+10 -4
View File
@@ -423,8 +423,11 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
if (typeof settings.showReasoningTraces === 'boolean' && settings.showReasoningTraces !== store.showReasoningTraces) { if (typeof settings.showReasoningTraces === 'boolean' && settings.showReasoningTraces !== store.showReasoningTraces) {
store.setShowReasoningTraces(settings.showReasoningTraces); store.setShowReasoningTraces(settings.showReasoningTraces);
} }
if (typeof settings.sessionAssistEnabled === 'boolean' && settings.sessionAssistEnabled !== store.sessionAssistEnabled) { if (typeof settings.sessionRecapEnabled === 'boolean' && settings.sessionRecapEnabled !== store.sessionRecapEnabled) {
store.setSessionAssistEnabled(settings.sessionAssistEnabled); store.setSessionRecapEnabled(settings.sessionRecapEnabled);
}
if (typeof settings.sessionSuggestionEnabled === 'boolean' && settings.sessionSuggestionEnabled !== store.sessionSuggestionEnabled) {
store.setSessionSuggestionEnabled(settings.sessionSuggestionEnabled);
} }
if (typeof settings.collapsibleThinkingBlocks === 'boolean' && settings.collapsibleThinkingBlocks !== store.collapsibleThinkingBlocks) { if (typeof settings.collapsibleThinkingBlocks === 'boolean' && settings.collapsibleThinkingBlocks !== store.collapsibleThinkingBlocks) {
store.setCollapsibleThinkingBlocks(settings.collapsibleThinkingBlocks); store.setCollapsibleThinkingBlocks(settings.collapsibleThinkingBlocks);
@@ -768,8 +771,11 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.showReasoningTraces === 'boolean') { if (typeof candidate.showReasoningTraces === 'boolean') {
result.showReasoningTraces = candidate.showReasoningTraces; result.showReasoningTraces = candidate.showReasoningTraces;
} }
if (typeof candidate.sessionAssistEnabled === 'boolean') { if (typeof candidate.sessionRecapEnabled === 'boolean') {
result.sessionAssistEnabled = candidate.sessionAssistEnabled; result.sessionRecapEnabled = candidate.sessionRecapEnabled;
}
if (typeof candidate.sessionSuggestionEnabled === 'boolean') {
result.sessionSuggestionEnabled = candidate.sessionSuggestionEnabled;
} }
if (typeof candidate.collapsibleThinkingBlocks === 'boolean') { if (typeof candidate.collapsibleThinkingBlocks === 'boolean') {
result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks; result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks;
+9 -3
View File
@@ -167,10 +167,16 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
keywords: ['streaming', 'sse', 'websocket'], keywords: ['streaming', 'sse', 'websocket'],
}, },
{ {
id: 'chat.session-assist', id: 'chat.session-recap',
page: 'chat', page: 'chat',
titleKey: 'settings.openchamber.visual.field.sessionAssist', titleKey: 'settings.openchamber.visual.field.sessionRecap',
keywords: ['recap', 'suggestion', 'assist', 'small model', 'summary'], keywords: ['recap', 'assist', 'small model', 'summary'],
},
{
id: 'chat.session-suggestion',
page: 'chat',
titleKey: 'settings.openchamber.visual.field.sessionSuggestion',
keywords: ['suggestion', 'assist', 'small model', 'follow up'],
}, },
{ {
id: 'chat.reasoning-traces', id: 'chat.reasoning-traces',
+14 -6
View File
@@ -560,7 +560,8 @@ interface UIStore {
eventStreamStatus: EventStreamStatus; eventStreamStatus: EventStreamStatus;
eventStreamHint: string | null; eventStreamHint: string | null;
showReasoningTraces: boolean; showReasoningTraces: boolean;
sessionAssistEnabled: boolean; sessionRecapEnabled: boolean;
sessionSuggestionEnabled: boolean;
collapsibleThinkingBlocks: boolean; collapsibleThinkingBlocks: boolean;
groupReasoningBlocks: boolean; groupReasoningBlocks: boolean;
chatRenderMode: ChatRenderMode; chatRenderMode: ChatRenderMode;
@@ -709,7 +710,8 @@ interface UIStore {
setSettingsRemoteInstancesSelectedId: (instanceId: string | null) => void; setSettingsRemoteInstancesSelectedId: (instanceId: string | null) => void;
setEventStreamStatus: (status: EventStreamStatus, hint?: string | null) => void; setEventStreamStatus: (status: EventStreamStatus, hint?: string | null) => void;
setShowReasoningTraces: (value: boolean) => void; setShowReasoningTraces: (value: boolean) => void;
setSessionAssistEnabled: (value: boolean) => void; setSessionRecapEnabled: (value: boolean) => void;
setSessionSuggestionEnabled: (value: boolean) => void;
setCollapsibleThinkingBlocks: (value: boolean) => void; setCollapsibleThinkingBlocks: (value: boolean) => void;
setChatRenderMode: (value: ChatRenderMode) => void; setChatRenderMode: (value: ChatRenderMode) => void;
setActivityRenderMode: (value: ActivityRenderMode) => void; setActivityRenderMode: (value: ActivityRenderMode) => void;
@@ -853,7 +855,8 @@ export const useUIStore = create<UIStore>()(
eventStreamStatus: 'idle', eventStreamStatus: 'idle',
eventStreamHint: null, eventStreamHint: null,
showReasoningTraces: true, showReasoningTraces: true,
sessionAssistEnabled: true, sessionRecapEnabled: true,
sessionSuggestionEnabled: true,
collapsibleThinkingBlocks: true, collapsibleThinkingBlocks: true,
groupReasoningBlocks: true, groupReasoningBlocks: true,
chatRenderMode: 'live', chatRenderMode: 'live',
@@ -1546,8 +1549,12 @@ export const useUIStore = create<UIStore>()(
set({ showReasoningTraces: value }); set({ showReasoningTraces: value });
}, },
setSessionAssistEnabled: (value) => { setSessionRecapEnabled: (value) => {
set({ sessionAssistEnabled: value }); set({ sessionRecapEnabled: value });
},
setSessionSuggestionEnabled: (value) => {
set({ sessionSuggestionEnabled: value });
}, },
setCollapsibleThinkingBlocks: (value) => { setCollapsibleThinkingBlocks: (value) => {
@@ -2234,7 +2241,8 @@ export const useUIStore = create<UIStore>()(
isSessionCreateDialogOpen: state.isSessionCreateDialogOpen, isSessionCreateDialogOpen: state.isSessionCreateDialogOpen,
// Note: isSettingsDialogOpen intentionally NOT persisted // Note: isSettingsDialogOpen intentionally NOT persisted
showReasoningTraces: state.showReasoningTraces, showReasoningTraces: state.showReasoningTraces,
sessionAssistEnabled: state.sessionAssistEnabled, sessionRecapEnabled: state.sessionRecapEnabled,
sessionSuggestionEnabled: state.sessionSuggestionEnabled,
collapsibleThinkingBlocks: state.collapsibleThinkingBlocks, collapsibleThinkingBlocks: state.collapsibleThinkingBlocks,
chatRenderMode: state.chatRenderMode, chatRenderMode: state.chatRenderMode,
activityRenderMode: state.activityRenderMode, activityRenderMode: state.activityRenderMode,
@@ -303,8 +303,12 @@ export const persistSettings = async (changes: Record<string, unknown>, ctx?: Br
delete restChanges.smallModelUseDefault; delete restChanges.smallModelUseDefault;
} }
if ('sessionAssistEnabled' in restChanges && typeof restChanges.sessionAssistEnabled !== 'boolean') { if ('sessionRecapEnabled' in restChanges && typeof restChanges.sessionRecapEnabled !== 'boolean') {
delete restChanges.sessionAssistEnabled; delete restChanges.sessionRecapEnabled;
}
if ('sessionSuggestionEnabled' in restChanges && typeof restChanges.sessionSuggestionEnabled !== 'boolean') {
delete restChanges.sessionSuggestionEnabled;
} }
if (typeof restChanges.usageAutoRefresh !== 'boolean') { if (typeof restChanges.usageAutoRefresh !== 'boolean') {
@@ -245,8 +245,11 @@ export const createSettingsHelpers = (dependencies) => {
if (typeof candidate.showReasoningTraces === 'boolean') { if (typeof candidate.showReasoningTraces === 'boolean') {
result.showReasoningTraces = candidate.showReasoningTraces; result.showReasoningTraces = candidate.showReasoningTraces;
} }
if (typeof candidate.sessionAssistEnabled === 'boolean') { if (typeof candidate.sessionRecapEnabled === 'boolean') {
result.sessionAssistEnabled = candidate.sessionAssistEnabled; result.sessionRecapEnabled = candidate.sessionRecapEnabled;
}
if (typeof candidate.sessionSuggestionEnabled === 'boolean') {
result.sessionSuggestionEnabled = candidate.sessionSuggestionEnabled;
} }
if (typeof candidate.collapsibleThinkingBlocks === 'boolean') { if (typeof candidate.collapsibleThinkingBlocks === 'boolean') {
result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks; 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 conversation content never goes to a provider the user didn't pick for
the session, unless the small model was chosen explicitly (settings the session, unless the small model was chosen explicitly (settings
override or opencode config). A resolver 404 is silently skipped. override or opencode config). A resolver 404 is silently skipped.
4. The `{recap, suggestion}` JSON is clamped and PATCHed onto the session 4. The requested JSON fields (`recap`, `suggestion`, or both) are clamped and
metadata together with `forMessageID` (the last assistant message id) and PATCHed onto the session metadata together with `forMessageID` (the last
`generatedAt`. Before writing, the session tail is re-checked (a stale assistant message id) and `generatedAt`. Before writing, the session tail is
result is dropped) and the metadata is merged from a fresh session read so re-checked (a stale result is dropped) and the metadata is merged from a
concurrent metadata writes made during generation are preserved. fresh session read so concurrent metadata writes made during generation are
preserved.
## Settings gate ## Settings gate
`sessionAssistEnabled` in OpenChamber settings (Settings → Chat, default on) `sessionRecapEnabled` and `sessionSuggestionEnabled` in OpenChamber settings
is a hard generation switch checked at fire time: when off, no small-model (Settings → Chat, default on) are hard generation switches checked at fire
calls run and nothing is written. Existing payloads keep rendering and can time. When both are off, no small-model calls run and nothing is written. When
still be dismissed — the switch is about generation, not visibility. 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) ## Freshness contract (no clearing writes)
@@ -47,7 +49,7 @@ everywhere instantly and offline; the next idle cycle overwrites it.
## UI consumers (packages/ui) ## UI consumers (packages/ui)
- `lib/sessionAssistMetadata.ts` — payload parsing. - `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). for the recap (single timeout to the boundary, no polling).
- `components/chat/SessionRecapSpacer.tsx` — renders the recap inside the - `components/chat/SessionRecapSpacer.tsx` — renders the recap inside the
fixed-height reserved gap under the last message (height never changes). fixed-height reserved gap under the last message (height never changes).
@@ -19,16 +19,19 @@ const OPENCHAMBER_SETTINGS_FILE = path.join(
'settings.json', 'settings.json',
); );
// The Chat setting is a hard generation switch (default on): when off, no // The Chat settings are hard generation switches (default on): when both are
// small-model calls and no metadata writes happen at all. Existing payloads // off, no small-model calls and no metadata writes happen at all. Existing
// stay untouched — clients keep showing them and dismissal still works. // payloads stay untouched — clients keep showing them and dismissal still works.
const isSessionAssistEnabled = () => { const getSessionAssistTargets = () => {
try { try {
const raw = fs.readFileSync(OPENCHAMBER_SETTINGS_FILE, 'utf8'); const raw = fs.readFileSync(OPENCHAMBER_SETTINGS_FILE, 'utf8');
const settings = JSON.parse(raw); const settings = JSON.parse(raw);
return settings?.sessionAssistEnabled !== false; return {
recap: settings?.sessionRecapEnabled !== false,
suggestion: settings?.sessionSuggestionEnabled !== false,
};
} catch { } catch {
return true; return { recap: true, suggestion: true };
} }
}; };
@@ -39,50 +42,52 @@ const RECAP_CHAR_LIMIT = 320;
const SUGGESTION_CHAR_LIMIT = 500; const SUGGESTION_CHAR_LIMIT = 500;
const FETCH_TIMEOUT_MS = 5_000; 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.', '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}', `Shape: {${[recap ? '"recap": string' : '', suggestion ? '"suggestion": string' : ''].filter(Boolean).join(', ')}}`,
'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.', recap
'suggestion: write ONE immediately sendable next user message addressed TO the coding agent.', ? '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.'
'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.', suggestion ? 'suggestion: write ONE immediately sendable next user message addressed TO the coding agent.' : '',
'Rules for suggestion:', 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.' : '',
'- Output exactly one message the user could click and send without editing.', 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.' : '',
'- Pick one best next action yourself.', suggestion ? 'Rules for suggestion:' : '',
'- Do not include alternatives, choices, slash-separated options, or "or".', suggestion ? '- Output exactly one message the user could click and send without editing.' : '',
'- Do not write "Do X or Y", "Ask whether...", "Maybe...", or "You could...".', suggestion ? '- Pick one best next action yourself.' : '',
'- Do not ask for information the assistant already provided.', suggestion ? '- Do not include alternatives, choices, slash-separated options, or "or".' : '',
'- 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 write "Do X or Y", "Ask whether...", "Maybe...", or "You could...".' : '',
'- Do not produce generic workflow commands like "Run tests" unless testing is clearly the next unresolved step.', suggestion ? '- Do not ask for information the assistant already provided.' : '',
'- Do not produce meta/debug requests that merely inspect the implementation.', 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.' : '',
'- Use imperative or question form.', suggestion ? '- Do not produce generic workflow commands like "Run tests" unless testing is clearly the next unresolved step.' : '',
'- Keep it concise.', suggestion ? '- Do not produce meta/debug requests that merely inspect the implementation.' : '',
'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 ? '- Use imperative or question form.' : '',
'Example 1:', suggestion ? '- Keep it concise.' : '',
'Assistant reply summary:', 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.' : '',
'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 ? 'Example 1:' : '',
'Bad suggestion:', suggestion ? 'Assistant reply summary:' : '',
'"Show me the exact runtime.js code and where the prompt is built."', 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.' : '',
'Why bad:', suggestion ? 'Bad suggestion:' : '',
'It asks for information the assistant already provided. It repeats inspection instead of moving to an improvement or decision.', suggestion ? '"Show me the exact runtime.js code and where the prompt is built."' : '',
'Good suggestion:', suggestion ? 'Why bad:' : '',
'"Suggest how to improve the prompt and context so the generated suggestion is more useful."', suggestion ? 'It asks for information the assistant already provided. It repeats inspection instead of moving to an improvement or decision.' : '',
'Why good:', suggestion ? 'Good suggestion:' : '',
'It naturally continues from the analysis and asks for a concrete improvement.', suggestion ? '"Suggest how to improve the prompt and context so the generated suggestion is more useful."' : '',
'Example 2:', suggestion ? 'Why good:' : '',
'Assistant reply summary:', suggestion ? 'It naturally continues from the analysis and asks for a concrete improvement.' : '',
'The assistant implemented a timeline dialog redesign, listed concrete UI changes, and reported that type-check and lint passed.', suggestion ? 'Example 2:' : '',
'Bad suggestion:', suggestion ? 'Assistant reply summary:' : '',
'"Check whether scrolling or loading older messages works without jumps."', suggestion ? 'The assistant implemented a timeline dialog redesign, listed concrete UI changes, and reported that type-check and lint passed.' : '',
'Why bad:', suggestion ? 'Bad suggestion:' : '',
'It contains an alternative. A suggestion chip must be one sendable message, not a choice the user has to edit.', suggestion ? '"Check whether scrolling or loading older messages works without jumps."' : '',
'Good suggestion:', suggestion ? 'Why bad:' : '',
'"Check whether scrolling and loading older messages work without jumps."', suggestion ? 'It contains an alternative. A suggestion chip must be one sendable message, not a choice the user has to edit.' : '',
'Why good:', suggestion ? 'Good suggestion:' : '',
'It picks a single validation request that the user can send immediately.', suggestion ? '"Check whether scrolling and loading older messages work without jumps."' : '',
'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.', 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.', 'Use double quotes for JSON strings, no trailing commas.',
].join('\n'); ].filter(Boolean).join('\n');
const extractJsonObject = (value) => { const extractJsonObject = (value) => {
const text = String(value ?? '').trim(); const text = String(value ?? '').trim();
@@ -192,7 +197,8 @@ export const createSessionAssistRuntime = ({
}; };
const generateAssist = async (sessionId, directory) => { 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 }) const session = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, { directory })
.catch((error) => { .catch((error) => {
console.warn(`[session-assist] session fetch failed: ${error?.message || error}`); console.warn(`[session-assist] session fetch failed: ${error?.message || error}`);
@@ -234,6 +240,9 @@ export const createSessionAssistRuntime = ({
if (!transcript) return; if (!transcript) return;
const { generateSmallModelText } = await getSmallModelService(); 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 // Instruct the language by example, not by description — account-side
// personalization (e.g. the ChatGPT backend knowing the user's locale) // personalization (e.g. the ChatGPT backend knowing the user's locale)
// otherwise leaks a different language into the output. // 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 // session's own provider unless the user explicitly picked a small
// model (settings override / opencode config). // model (settings override / opencode config).
restrictToPreferredProvider: true, 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}"`, 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: ASSIST_SYSTEM_PROMPT, system: buildAssistSystemPrompt(targets),
directory, directory,
preferredProviderID: typeof lastAssistantInfo.providerID === 'string' ? lastAssistantInfo.providerID : undefined, preferredProviderID: typeof lastAssistantInfo.providerID === 'string' ? lastAssistantInfo.providerID : undefined,
preferredModelID: typeof lastAssistantInfo.modelID === 'string' ? lastAssistantInfo.modelID : undefined, preferredModelID: typeof lastAssistantInfo.modelID === 'string' ? lastAssistantInfo.modelID : undefined,
@@ -261,8 +270,8 @@ export const createSessionAssistRuntime = ({
} }
const structured = extractJsonObject(generated?.text); const structured = extractJsonObject(generated?.text);
let recap = typeof structured?.recap === 'string' ? structured.recap.trim().slice(0, RECAP_CHAR_LIMIT) : ''; let recap = targets.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 suggestion = targets.suggestion && typeof structured?.suggestion === 'string' ? structured.suggestion.trim().slice(0, SUGGESTION_CHAR_LIMIT) : '';
// Hard guard against language hallucination: if the conversation contains // Hard guard against language hallucination: if the conversation contains
// no Cyrillic/CJK at all, the output must not either (and drop per-field, // no Cyrillic/CJK at all, the output must not either (and drop per-field,