From 4881cf9aee719e263de8ea4f62fdd6def8853d2c Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:20:53 -0700 Subject: [PATCH] fix(catchup): the strip earns its row + summaries move to deepseek-v4-flash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sam's revised ruling, hours after #1453 shipped: 'I don't like that catch-up… reveals no good info at all, and always shows up is not a good design, unless we use DeepSeek flash the quality might be a little better.' Three changes: (1) the strip renders ONLY when a summary exists, is under 24h old, and its version has not been dismissed — no empty shell, no Summarize row; (2) a dismiss control, persisted per summary version in localStorage (wrapped, degrades to not-dismissed); (3) LITELLM_CHAT_MODEL flips to deepseek-v4-flash — already provisioned in LiteLLM with a live key (the guide agent's model). Tests rewritten to the earn-your-row contract; both locales carry the dismiss string. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TdEJoXUmbHmW5TFk7hfkbK --- frontend/src/i18n/locales/en.json | 3 +- frontend/src/i18n/locales/zh-CN.json | 3 +- .../src/v2/__tests__/V2CatchUpStrip.test.tsx | 41 +++++++++++++++---- frontend/src/v2/components/V2CatchUpStrip.tsx | 35 +++++++++++++++- frontend/src/v2/v2.css | 10 +++++ .../templates/core/backend-deployment.yaml | 5 ++- 6 files changed, 84 insertions(+), 13 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 00074e807..fac622052 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -450,7 +450,8 @@ "empty": "No summary yet", "summarize": "Summarize", "refresh": "Refresh", - "working": "Summarizing…" + "working": "Summarizing…", + "dismiss": "Dismiss" } }, "landing": { diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 92c05b472..3f157374f 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -450,7 +450,8 @@ "empty": "还没有摘要", "summarize": "生成摘要", "refresh": "刷新", - "working": "正在总结…" + "working": "正在总结…", + "dismiss": "关闭" } }, "landing": { diff --git a/frontend/src/v2/__tests__/V2CatchUpStrip.test.tsx b/frontend/src/v2/__tests__/V2CatchUpStrip.test.tsx index c2d1184c4..061a35377 100644 --- a/frontend/src/v2/__tests__/V2CatchUpStrip.test.tsx +++ b/frontend/src/v2/__tests__/V2CatchUpStrip.test.tsx @@ -44,20 +44,43 @@ describe('V2CatchUpStrip', () => { expect(screen.getByTestId('catchup-body')).toHaveTextContent('Line one. Line two.'); }); - test('a pod with no summary offers Summarize; refresh swaps in the generated summary', async () => { + // Sam's revised ruling, hours after the strip shipped (2026-09-01): "always + // shows up is not a good design" — the strip EARNS its row. No summary, a + // stale summary, or a dismissed one ⇒ no strip at all. + test('a pod with no summary renders NO strip', async () => { axios.get.mockResolvedValueOnce({ data: null }); - axios.post.mockResolvedValueOnce({ data: { summary: { content: 'Fresh digest.', createdAt: new Date().toISOString() } } }); render(); - await waitFor(() => expect(screen.getByText('No summary yet')).toBeInTheDocument()); - fireEvent.click(screen.getByRole('button', { name: 'Summarize' })); - await waitFor(() => expect(screen.getByTestId('catchup-body')).toHaveTextContent('Fresh digest.')); - expect(axios.post).toHaveBeenCalledWith('/api/summaries/pod/p2/refresh', {}); + await waitFor(() => expect(axios.get).toHaveBeenCalled()); + expect(screen.queryByTestId('catchup-strip')).toBeNull(); }); - test('a failed summary read never blocks the chat — strip still renders', async () => { + test('a summary older than 24h renders NO strip', async () => { + axios.get.mockResolvedValueOnce({ + data: { content: 'Old news.', createdAt: new Date(Date.now() - 25 * 3600000).toISOString() }, + }); + render(); + await waitFor(() => expect(axios.get).toHaveBeenCalled()); + expect(screen.queryByTestId('catchup-strip')).toBeNull(); + }); + + test('dismiss hides this summary version and persists; the strip stays gone on re-render', async () => { + const createdAt = new Date(Date.now() - 5 * 60000).toISOString(); + axios.get.mockResolvedValue({ data: { content: 'Digest.', createdAt } }); + const { unmount } = render(); + await waitFor(() => expect(screen.getByTestId('catchup-strip')).toBeInTheDocument()); + fireEvent.click(screen.getByRole('button', { name: 'Dismiss' })); + expect(screen.queryByTestId('catchup-strip')).toBeNull(); + expect(window.localStorage.getItem('v2.catchup.dismissed.p5')).toBe(createdAt); + unmount(); + render(); + await waitFor(() => expect(axios.get).toHaveBeenCalledTimes(2)); + expect(screen.queryByTestId('catchup-strip')).toBeNull(); + }); + + test('a failed summary read never blocks the chat — and renders no strip', async () => { axios.get.mockRejectedValueOnce(new Error('403')); render(); - await waitFor(() => expect(screen.getByTestId('catchup-strip')).toBeInTheDocument()); - expect(screen.getByText('No summary yet')).toBeInTheDocument(); + await waitFor(() => expect(axios.get).toHaveBeenCalled()); + expect(screen.queryByTestId('catchup-strip')).toBeNull(); }); }); diff --git a/frontend/src/v2/components/V2CatchUpStrip.tsx b/frontend/src/v2/components/V2CatchUpStrip.tsx index 61b3e96b4..78f53d95c 100644 --- a/frontend/src/v2/components/V2CatchUpStrip.tsx +++ b/frontend/src/v2/components/V2CatchUpStrip.tsx @@ -75,7 +75,31 @@ const V2CatchUpStrip: React.FC = ({ podId }) => { } }, [podId, refreshing]); + // Dismissal is per summary VERSION: dismissing hides this summary, and the + // strip returns only when a newer one exists. localStorage per the browser + // storage rules — wrapped, and absence just means "not dismissed". + const [dismissed, setDismissed] = useState(false); + useEffect(() => { + try { + setDismissed(window.localStorage.getItem(`v2.catchup.dismissed.${podId}`) === (summary?.createdAt || '')); + } catch { setDismissed(false); } + }, [podId, summary]); + const handleDismiss = useCallback(() => { + try { window.localStorage.setItem(`v2.catchup.dismissed.${podId}`, summary?.createdAt || ''); } catch { /* per-viewer convenience only */ } + setDismissed(true); + }, [podId, summary]); + if (!loaded) return null; + // Sam's revised ruling (2026-09-01, hours after the strip shipped): + // "always shows up is not a good design" and an empty/stale strip + // "reveals no good info". So the strip EARNS its row: it renders only + // when a summary exists, is fresh (24h), and this version has not been + // dismissed. No summary -> no strip, not an empty shell with a + // Summarize button — generating one on demand stays available from the + // inspector/summaries surface. + const FRESH_MS = 24 * 60 * 60 * 1000; + const isFresh = !!(summary?.createdAt && Date.now() - new Date(summary.createdAt).getTime() < FRESH_MS); + if (!summary?.content || !isFresh || dismissed) return null; return (
@@ -108,7 +132,16 @@ const V2CatchUpStrip: React.FC = ({ podId }) => { > {refreshing ? t('podChat.catchup.working') - : summary ? t('podChat.catchup.refresh') : t('podChat.catchup.summarize')} + : t('podChat.catchup.refresh')} + +
{expanded && summary?.content && ( diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 9a5376df5..ce1df52f1 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -1715,6 +1715,16 @@ body.modern-ui.v2-canvas { } .v2-root button.v2-catchup__refresh:hover:not(:disabled) { text-decoration: underline; } .v2-root button.v2-catchup__refresh:disabled { color: var(--v2-text-tertiary); cursor: default; } +.v2-root button.v2-catchup__dismiss { + background: transparent; + border: none; + padding: 0 2px; + font-size: 14px; + line-height: 1; + color: var(--v2-text-tertiary); + cursor: pointer; +} +.v2-root button.v2-catchup__dismiss:hover { color: var(--v2-text); } .v2-catchup__body { margin-top: 6px; max-height: 220px; diff --git a/k8s/helm/commonly/templates/core/backend-deployment.yaml b/k8s/helm/commonly/templates/core/backend-deployment.yaml index 94f5b3eea..14897a220 100644 --- a/k8s/helm/commonly/templates/core/backend-deployment.yaml +++ b/k8s/helm/commonly/templates/core/backend-deployment.yaml @@ -419,7 +419,10 @@ spec: - name: LITELLM_BASE_URL value: "http://litellm:4000" - name: LITELLM_CHAT_MODEL - value: "chatgpt/gpt-5.4-mini" + # deepseek-v4-flash (Sam, 2026-09-01): summary quality on + # gpt-5.4-mini was "no good info at all"; the DeepSeek flash + # entry already exists in LiteLLM with a live key. + value: "deepseek-v4-flash" # Temporary: bypass LiteLLM's broken chatgpt/ bridge (BerriAI/litellm#25429) # and route Codex directly via OpenClaw's native openai-codex-responses handler. # Flip to "false" / remove once LiteLLM ships a fix for empty-output responses.