Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,8 @@
"empty": "No summary yet",
"summarize": "Summarize",
"refresh": "Refresh",
"working": "Summarizing…"
"working": "Summarizing…",
"dismiss": "Dismiss"
}
},
"landing": {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,8 @@
"empty": "还没有摘要",
"summarize": "生成摘要",
"refresh": "刷新",
"working": "正在总结…"
"working": "正在总结…",
"dismiss": "关闭"
}
},
"landing": {
Expand Down
41 changes: 32 additions & 9 deletions frontend/src/v2/__tests__/V2CatchUpStrip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<V2CatchUpStrip podId="p2" />);
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(<V2CatchUpStrip podId="p4" />);
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(<V2CatchUpStrip podId="p5" />);
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(<V2CatchUpStrip podId="p5" />);
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(<V2CatchUpStrip podId="p3" />);
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();
});
});
35 changes: 34 additions & 1 deletion frontend/src/v2/components/V2CatchUpStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,31 @@ const V2CatchUpStrip: React.FC<Props> = ({ 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 (
<div className="v2-catchup" data-testid="catchup-strip">
Expand Down Expand Up @@ -108,7 +132,16 @@ const V2CatchUpStrip: React.FC<Props> = ({ podId }) => {
>
{refreshing
? t('podChat.catchup.working')
: summary ? t('podChat.catchup.refresh') : t('podChat.catchup.summarize')}
: t('podChat.catchup.refresh')}
</button>
<button
type="button"
className="v2-catchup__dismiss"
onClick={handleDismiss}
aria-label={t('podChat.catchup.dismiss')}
title={t('podChat.catchup.dismiss')}
>
×
</button>
</div>
{expanded && summary?.content && (
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/v2/v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion k8s/helm/commonly/templates/core/backend-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading