From 182fc89b8d0a355432a33f33775180d1104ca59d Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 6 Jul 2026 22:54:31 +0300 Subject: [PATCH] feat: split session recap and suggestion settings --- .../components/chat/SessionRecapSpacer.tsx | 2 +- .../openchamber/OpenChamberVisualSettings.tsx | 45 +++++-- packages/ui/src/hooks/useSessionAssist.ts | 11 +- packages/ui/src/lib/appearanceAutoSave.ts | 16 ++- packages/ui/src/lib/desktop.ts | 3 +- .../ui/src/lib/i18n/messages/en.settings.ts | 6 +- .../ui/src/lib/i18n/messages/es.settings.ts | 6 +- .../ui/src/lib/i18n/messages/fr.settings.ts | 6 +- .../ui/src/lib/i18n/messages/ja.settings.ts | 6 +- .../ui/src/lib/i18n/messages/ko.settings.ts | 6 +- .../ui/src/lib/i18n/messages/pl.settings.ts | 6 +- .../src/lib/i18n/messages/pt-BR.settings.ts | 6 +- .../ui/src/lib/i18n/messages/uk.settings.ts | 6 +- .../src/lib/i18n/messages/zh-CN.settings.ts | 6 +- .../src/lib/i18n/messages/zh-TW.settings.ts | 6 +- packages/ui/src/lib/persistence.ts | 14 ++- packages/ui/src/lib/settings/search.ts | 12 +- packages/ui/src/stores/useUIStore.ts | 20 ++- .../vscode/src/bridge-settings-runtime.ts | 8 +- .../server/lib/opencode/settings-helpers.js | 7 +- .../lib/session-assist/DOCUMENTATION.md | 22 ++-- .../web/server/lib/session-assist/runtime.js | 115 ++++++++++-------- 22 files changed, 214 insertions(+), 121 deletions(-) diff --git a/packages/ui/src/components/chat/SessionRecapSpacer.tsx b/packages/ui/src/components/chat/SessionRecapSpacer.tsx index 74c160bc..8cb990aa 100644 --- a/packages/ui/src/components/chat/SessionRecapSpacer.tsx +++ b/packages/ui/src/components/chat/SessionRecapSpacer.tsx @@ -10,7 +10,7 @@ interface SessionRecapNoteProps { // 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 -// 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 = React.memo(({ sessionId, directory, isMobile }) => { const { visibleRecap } = useSessionAssistState(sessionId, directory); const { t } = useI18n(); diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index a9cf147b..e75ddeda 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -259,8 +259,10 @@ export const OpenChamberVisualSettings: React.FC const { browserTab } = usePwaDetection(); const directoryShowHidden = useDirectoryShowHidden(); const showReasoningTraces = useUIStore(state => state.showReasoningTraces); - const sessionAssistEnabled = useUIStore(state => state.sessionAssistEnabled); - const setSessionAssistEnabled = useUIStore(state => state.setSessionAssistEnabled); + const sessionRecapEnabled = useUIStore(state => state.sessionRecapEnabled); + 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 collapsibleThinkingBlocks = useUIStore(state => state.collapsibleThinkingBlocks); const setCollapsibleThinkingBlocks = useUIStore(state => state.setCollapsibleThinkingBlocks); @@ -1780,27 +1782,50 @@ export const OpenChamberVisualSettings: React.FC {(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') && ( + <>
setSessionAssistEnabled(!sessionAssistEnabled)} + aria-pressed={sessionRecapEnabled} + onClick={() => setSessionRecapEnabled(!sessionRecapEnabled)} onKeyDown={(event) => { if (event.key === ' ' || event.key === 'Enter') { event.preventDefault(); - setSessionAssistEnabled(!sessionAssistEnabled); + setSessionRecapEnabled(!sessionRecapEnabled); } }} > - {t('settings.openchamber.visual.field.sessionAssist')} + {t('settings.openchamber.visual.field.sessionRecap')}
+
setSessionSuggestionEnabled(!sessionSuggestionEnabled)} + onKeyDown={(event) => { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + setSessionSuggestionEnabled(!sessionSuggestionEnabled); + } + }} + > + + {t('settings.openchamber.visual.field.sessionSuggestion')} +
+ )} {shouldShow('reasoning') && (
state.sessionRecapEnabled); + const sessionSuggestionEnabled = useUIStore((state) => state.sessionSuggestionEnabled); const isIdle = !status || status.type === 'idle'; const payload = getSessionAssist(session); @@ -88,7 +91,7 @@ export function useSessionAssistState(sessionId: string, directory?: string): Se return { assist, - visibleRecap: assist && assist.recap && quietElapsed ? assist.recap : null, - suggestion: assist && assist.suggestion ? assist.suggestion : null, + visibleRecap: sessionRecapEnabled && assist && assist.recap && quietElapsed ? assist.recap : null, + suggestion: sessionSuggestionEnabled && assist && assist.suggestion ? assist.suggestion : null, }; } diff --git a/packages/ui/src/lib/appearanceAutoSave.ts b/packages/ui/src/lib/appearanceAutoSave.ts index ab083612..7ac1b1a2 100644 --- a/packages/ui/src/lib/appearanceAutoSave.ts +++ b/packages/ui/src/lib/appearanceAutoSave.ts @@ -6,7 +6,8 @@ import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode'; type AppearanceSlice = { showReasoningTraces: boolean; - sessionAssistEnabled: boolean; + sessionRecapEnabled: boolean; + sessionSuggestionEnabled: boolean; collapsibleThinkingBlocks: boolean; showDeletionDialog: boolean; nativeNotificationsEnabled: boolean; @@ -51,7 +52,8 @@ export const startAppearanceAutoSave = (): void => { let previous: AppearanceSlice = { showReasoningTraces: useUIStore.getState().showReasoningTraces, - sessionAssistEnabled: useUIStore.getState().sessionAssistEnabled, + sessionRecapEnabled: useUIStore.getState().sessionRecapEnabled, + sessionSuggestionEnabled: useUIStore.getState().sessionSuggestionEnabled, collapsibleThinkingBlocks: useUIStore.getState().collapsibleThinkingBlocks, showDeletionDialog: useUIStore.getState().showDeletionDialog, nativeNotificationsEnabled: useUIStore.getState().nativeNotificationsEnabled, @@ -103,7 +105,8 @@ export const startAppearanceAutoSave = (): void => { useUIStore.subscribe((state) => { const current: AppearanceSlice = { showReasoningTraces: state.showReasoningTraces, - sessionAssistEnabled: state.sessionAssistEnabled, + sessionRecapEnabled: state.sessionRecapEnabled, + sessionSuggestionEnabled: state.sessionSuggestionEnabled, collapsibleThinkingBlocks: state.collapsibleThinkingBlocks, showDeletionDialog: state.showDeletionDialog, nativeNotificationsEnabled: state.nativeNotificationsEnabled, @@ -137,8 +140,11 @@ export const startAppearanceAutoSave = (): void => { if (current.showReasoningTraces !== previous.showReasoningTraces) { diff.showReasoningTraces = current.showReasoningTraces; } - if (current.sessionAssistEnabled !== previous.sessionAssistEnabled) { - diff.sessionAssistEnabled = current.sessionAssistEnabled; + if (current.sessionRecapEnabled !== previous.sessionRecapEnabled) { + diff.sessionRecapEnabled = current.sessionRecapEnabled; + } + if (current.sessionSuggestionEnabled !== previous.sessionSuggestionEnabled) { + diff.sessionSuggestionEnabled = current.sessionSuggestionEnabled; } if (current.collapsibleThinkingBlocks !== previous.collapsibleThinkingBlocks) { diff.collapsibleThinkingBlocks = current.collapsibleThinkingBlocks; diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index fd2de600..095ce9da 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -114,7 +114,8 @@ export type DesktopSettings = { defaultVariant?: string; defaultAgent?: string; smallModelUseDefault?: boolean; - sessionAssistEnabled?: boolean; + sessionRecapEnabled?: boolean; + sessionSuggestionEnabled?: boolean; smallModelOverride?: string; // format: "provider/model" defaultGitIdentityId?: string; // ''/undefined = unset, 'global' or profile id openInAppId?: string; diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 9deabdc1..00513280 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1690,8 +1690,10 @@ export const settingsDict = { 'settings.openchamber.visual.field.userMessageRenderingAria': 'User message rendering: {option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid rendering: {option}', 'settings.openchamber.visual.field.diffLayoutAria': 'Diff layout: {option}', - 'settings.openchamber.visual.field.sessionAssist': 'Generate Session Recap & Suggestion', - 'settings.openchamber.visual.field.sessionAssistAria': 'Generate a recap and a suggested reply after the agent finishes', + 'settings.openchamber.visual.field.sessionRecap': 'Generate Session Recap', + '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.showReasoningTraces': 'Show Reasoning Traces', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Enable collapsible reasoning blocks', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index ad6cb599..654832c4 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1657,8 +1657,10 @@ export const settingsDict = { "settings.openchamber.visual.field.userMessageRenderingAria": "Renderizado de mensajes del usuario: {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.sessionAssist": "Generar resumen y sugerencia de sesión", - "settings.openchamber.visual.field.sessionAssistAria": "Generar un resumen y una respuesta sugerida cuando el agente termina", + "settings.openchamber.visual.field.sessionRecap": "Generar resumen de sesión", + "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.showReasoningTraces": "Mostrar trazas de razonamiento", "settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar bloques de razonamiento colapsables", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index c3722240..ac682324 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -1624,8 +1624,10 @@ export const settingsDict = { 'settings.openchamber.visual.field.userMessageRenderingAria': 'Rendu du message utilisateur : {option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Rendu Mermaid : {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.sessionAssistAria': "Générer un récapitulatif et une réponse suggérée quand l'agent termine", + 'settings.openchamber.visual.field.sessionRecap': 'Générer le récapitulatif de session', + '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.showReasoningTraces': 'Afficher les traces de raisonnement', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': 'Activer les blocs de raisonnement pliables', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index e4ea9ca1..0234e01c 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -1690,8 +1690,10 @@ export const settingsDict = { 'settings.openchamber.visual.field.userMessageRenderingAria': 'ユーザーメッセージ表示: {option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 表示: {option}', 'settings.openchamber.visual.field.diffLayoutAria': 'Diff レイアウト: {option}', - 'settings.openchamber.visual.field.sessionAssist': 'セッションの要約と提案を生成', - 'settings.openchamber.visual.field.sessionAssistAria': 'エージェントの完了後に要約と返信の提案を生成します', + 'settings.openchamber.visual.field.sessionRecap': 'セッションの要約を生成', + 'settings.openchamber.visual.field.sessionRecapAria': 'エージェントの完了後に要約を生成します', + 'settings.openchamber.visual.field.sessionSuggestion': '次のユーザーメッセージの提案を生成', + 'settings.openchamber.visual.field.sessionSuggestionAria': 'エージェントの完了後に次のユーザーメッセージの提案を生成します', 'settings.openchamber.visual.field.showReasoningTracesAria': '推論トレースを表示', 'settings.openchamber.visual.field.showReasoningTraces': '推論トレースを表示', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '折りたたみ可能な推論ブロックを有効化', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 3a24a088..7b48d10e 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1657,8 +1657,10 @@ export const settingsDict = { 'settings.openchamber.visual.field.userMessageRenderingAria': '사용자 메시지 렌더링: {option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 렌더링: {option}', 'settings.openchamber.visual.field.diffLayoutAria': 'Diff 레이아웃: {option}', - 'settings.openchamber.visual.field.sessionAssist': '세션 요약 및 제안 생성', - 'settings.openchamber.visual.field.sessionAssistAria': '에이전트가 완료되면 요약과 제안 답장을 생성합니다', + 'settings.openchamber.visual.field.sessionRecap': '세션 요약 생성', + '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.showReasoningTraces': 'Reasoning Trace 표시', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '접을 수 있는 추론 블록 활성화', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 75a2f201..b0be44bc 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -974,8 +974,10 @@ export const settingsDict = { 'settings.openchamber.visual.field.showDotfilesAria': 'Pokaż pliki ukryte', '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.sessionAssist': 'Generuj podsumowanie i sugestię sesji', - 'settings.openchamber.visual.field.sessionAssistAria': 'Generuj podsumowanie i sugerowaną odpowiedź po zakończeniu pracy agenta', + 'settings.openchamber.visual.field.sessionRecap': 'Generuj podsumowanie sesji', + '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.showReasoningTracesAria': 'Pokaż ślady rozumowania', 'settings.openchamber.visual.field.collapsibleThinkingBlocks': 'Włącz zwijalne bloki rozumowania', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index 453bfb72..2a6a8e6b 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1657,8 +1657,10 @@ export const settingsDict = { "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.diffLayoutAria": "Layout de comparação: {option}", - "settings.openchamber.visual.field.sessionAssist": "Gerar resumo e sugestão da sessão", - "settings.openchamber.visual.field.sessionAssistAria": "Gerar um resumo e uma resposta sugerida quando o agente termina", + "settings.openchamber.visual.field.sessionRecap": "Gerar resumo da sessão", + "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.showReasoningTraces": "Mostrar rastros de raciocínio", "settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Habilitar blocos de raciocínio recolhíveis", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 7b030311..24488a18 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1657,8 +1657,10 @@ export const settingsDict = { "settings.openchamber.visual.field.userMessageRenderingAria": "Відображення повідомлень користувача: {option}", "settings.openchamber.visual.field.mermaidRenderingAria": "Візуалізація Mermaid: {option}", "settings.openchamber.visual.field.diffLayoutAria": "Компонування diff: {option}", - "settings.openchamber.visual.field.sessionAssist": "Генерувати підсумок і пропозицію для сесії", - "settings.openchamber.visual.field.sessionAssistAria": "Генерувати підсумок і запропоновану відповідь після завершення роботи агента", + "settings.openchamber.visual.field.sessionRecap": "Генерувати підсумок сесії", + "settings.openchamber.visual.field.sessionRecapAria": "Генерувати підсумок після завершення роботи агента", + "settings.openchamber.visual.field.sessionSuggestion": "Генерувати пропозицію наступного повідомлення користувача", + "settings.openchamber.visual.field.sessionSuggestionAria": "Генерувати запропоноване наступне повідомлення користувача після завершення роботи агента", "settings.openchamber.visual.field.showReasoningTracesAria": "Показати сліди міркувань", "settings.openchamber.visual.field.showReasoningTraces": "Показати сліди міркувань", "settings.openchamber.visual.field.collapsibleThinkingBlocksAria": "Увімкнути згортальні блоки міркувань", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index defb89ce..e5e99b9d 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1657,8 +1657,10 @@ export const settingsDict = { 'settings.openchamber.visual.field.userMessageRenderingAria': '用户消息渲染:{option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 渲染:{option}', 'settings.openchamber.visual.field.diffLayoutAria': '差异布局:{option}', - 'settings.openchamber.visual.field.sessionAssist': '生成会话回顾与建议', - 'settings.openchamber.visual.field.sessionAssistAria': '代理完成后生成回顾和建议回复', + 'settings.openchamber.visual.field.sessionRecap': '生成会话回顾', + 'settings.openchamber.visual.field.sessionRecapAria': '代理完成后生成回顾', + 'settings.openchamber.visual.field.sessionSuggestion': '生成下一条用户消息建议', + 'settings.openchamber.visual.field.sessionSuggestionAria': '代理完成后生成下一条用户消息建议', 'settings.openchamber.visual.field.showReasoningTracesAria': '显示推理轨迹', 'settings.openchamber.visual.field.showReasoningTraces': '显示推理轨迹', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '启用可折叠推理块', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 0be05ab2..c75dd345 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1573,8 +1573,10 @@ 'settings.openchamber.visual.field.userMessageRenderingAria': '使用者訊息渲染:{option}', 'settings.openchamber.visual.field.mermaidRenderingAria': 'Mermaid 渲染:{option}', 'settings.openchamber.visual.field.diffLayoutAria': '差異佈局:{option}', - 'settings.openchamber.visual.field.sessionAssist': '產生工作階段回顧與建議', - 'settings.openchamber.visual.field.sessionAssistAria': '代理完成後產生回顧與建議回覆', + 'settings.openchamber.visual.field.sessionRecap': '產生工作階段回顧', + 'settings.openchamber.visual.field.sessionRecapAria': '代理完成後產生回顧', + 'settings.openchamber.visual.field.sessionSuggestion': '產生下一則使用者訊息建議', + 'settings.openchamber.visual.field.sessionSuggestionAria': '代理完成後產生下一則使用者訊息建議', 'settings.openchamber.visual.field.showReasoningTracesAria': '顯示推理軌跡', 'settings.openchamber.visual.field.showReasoningTraces': '顯示推理軌跡', 'settings.openchamber.visual.field.collapsibleThinkingBlocksAria': '啟用可摺疊推理區塊', diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index d7ccf5d0..4f7fec0e 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -423,8 +423,11 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => { if (typeof settings.showReasoningTraces === 'boolean' && settings.showReasoningTraces !== store.showReasoningTraces) { store.setShowReasoningTraces(settings.showReasoningTraces); } - if (typeof settings.sessionAssistEnabled === 'boolean' && settings.sessionAssistEnabled !== store.sessionAssistEnabled) { - store.setSessionAssistEnabled(settings.sessionAssistEnabled); + if (typeof settings.sessionRecapEnabled === 'boolean' && settings.sessionRecapEnabled !== store.sessionRecapEnabled) { + 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) { store.setCollapsibleThinkingBlocks(settings.collapsibleThinkingBlocks); @@ -768,8 +771,11 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { 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; diff --git a/packages/ui/src/lib/settings/search.ts b/packages/ui/src/lib/settings/search.ts index 82149835..c272b203 100644 --- a/packages/ui/src/lib/settings/search.ts +++ b/packages/ui/src/lib/settings/search.ts @@ -167,10 +167,16 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [ keywords: ['streaming', 'sse', 'websocket'], }, { - id: 'chat.session-assist', + id: 'chat.session-recap', page: 'chat', - titleKey: 'settings.openchamber.visual.field.sessionAssist', - keywords: ['recap', 'suggestion', 'assist', 'small model', 'summary'], + titleKey: 'settings.openchamber.visual.field.sessionRecap', + 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', diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 1daf530a..23f5870a 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -560,7 +560,8 @@ interface UIStore { eventStreamStatus: EventStreamStatus; eventStreamHint: string | null; showReasoningTraces: boolean; - sessionAssistEnabled: boolean; + sessionRecapEnabled: boolean; + sessionSuggestionEnabled: boolean; collapsibleThinkingBlocks: boolean; groupReasoningBlocks: boolean; chatRenderMode: ChatRenderMode; @@ -709,7 +710,8 @@ interface UIStore { setSettingsRemoteInstancesSelectedId: (instanceId: string | null) => void; setEventStreamStatus: (status: EventStreamStatus, hint?: string | null) => void; setShowReasoningTraces: (value: boolean) => void; - setSessionAssistEnabled: (value: boolean) => void; + setSessionRecapEnabled: (value: boolean) => void; + setSessionSuggestionEnabled: (value: boolean) => void; setCollapsibleThinkingBlocks: (value: boolean) => void; setChatRenderMode: (value: ChatRenderMode) => void; setActivityRenderMode: (value: ActivityRenderMode) => void; @@ -853,7 +855,8 @@ export const useUIStore = create()( eventStreamStatus: 'idle', eventStreamHint: null, showReasoningTraces: true, - sessionAssistEnabled: true, + sessionRecapEnabled: true, + sessionSuggestionEnabled: true, collapsibleThinkingBlocks: true, groupReasoningBlocks: true, chatRenderMode: 'live', @@ -1546,8 +1549,12 @@ export const useUIStore = create()( set({ showReasoningTraces: value }); }, - setSessionAssistEnabled: (value) => { - set({ sessionAssistEnabled: value }); + setSessionRecapEnabled: (value) => { + set({ sessionRecapEnabled: value }); + }, + + setSessionSuggestionEnabled: (value) => { + set({ sessionSuggestionEnabled: value }); }, setCollapsibleThinkingBlocks: (value) => { @@ -2234,7 +2241,8 @@ export const useUIStore = create()( isSessionCreateDialogOpen: state.isSessionCreateDialogOpen, // Note: isSettingsDialogOpen intentionally NOT persisted showReasoningTraces: state.showReasoningTraces, - sessionAssistEnabled: state.sessionAssistEnabled, + sessionRecapEnabled: state.sessionRecapEnabled, + sessionSuggestionEnabled: state.sessionSuggestionEnabled, collapsibleThinkingBlocks: state.collapsibleThinkingBlocks, chatRenderMode: state.chatRenderMode, activityRenderMode: state.activityRenderMode, diff --git a/packages/vscode/src/bridge-settings-runtime.ts b/packages/vscode/src/bridge-settings-runtime.ts index 86b80022..4c4242fd 100644 --- a/packages/vscode/src/bridge-settings-runtime.ts +++ b/packages/vscode/src/bridge-settings-runtime.ts @@ -303,8 +303,12 @@ export const persistSettings = async (changes: Record, ctx?: Br delete restChanges.smallModelUseDefault; } - if ('sessionAssistEnabled' in restChanges && typeof restChanges.sessionAssistEnabled !== 'boolean') { - delete restChanges.sessionAssistEnabled; + if ('sessionRecapEnabled' in restChanges && typeof restChanges.sessionRecapEnabled !== 'boolean') { + delete restChanges.sessionRecapEnabled; + } + + if ('sessionSuggestionEnabled' in restChanges && typeof restChanges.sessionSuggestionEnabled !== 'boolean') { + delete restChanges.sessionSuggestionEnabled; } if (typeof restChanges.usageAutoRefresh !== 'boolean') { diff --git a/packages/web/server/lib/opencode/settings-helpers.js b/packages/web/server/lib/opencode/settings-helpers.js index e868449e..056a5f04 100644 --- a/packages/web/server/lib/opencode/settings-helpers.js +++ b/packages/web/server/lib/opencode/settings-helpers.js @@ -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; diff --git a/packages/web/server/lib/session-assist/DOCUMENTATION.md b/packages/web/server/lib/session-assist/DOCUMENTATION.md index 72f2bcf1..ac8e58ec 100644 --- a/packages/web/server/lib/session-assist/DOCUMENTATION.md +++ b/packages/web/server/lib/session-assist/DOCUMENTATION.md @@ -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). diff --git a/packages/web/server/lib/session-assist/runtime.js b/packages/web/server/lib/session-assist/runtime.js index 7f477a07..ab4b1f45 100644 --- a/packages/web/server/lib/session-assist/runtime.js +++ b/packages/web/server/lib/session-assist/runtime.js @@ -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,