Skip to content

Commit fca7ff4

Browse files
fix(studio): address code review feedback for AI Chat panel
- Fix stale closure in toggle/keyboard handler using functional state updates - Memoize loadMessages() to avoid running on every render - Remove `as any` casts in tests by properly typing makeMsg helper Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/2f8d08c4-2f0a-4f27-998c-b4c4a8805883 Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
1 parent 03266e8 commit fca7ff4

3 files changed

Lines changed: 22 additions & 11 deletions

File tree

apps/studio/src/components/AiChatPanel.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@ export function AiChatPanel() {
3232
const inputRef = useRef<HTMLTextAreaElement>(null);
3333
const baseUrl = getApiBaseUrl();
3434

35+
const initialMessages = useMemo(() => loadMessages() as UIMessage[], []);
36+
3537
const transport = useMemo(
3638
() => new DefaultChatTransport({ api: `${baseUrl}/api/v1/ai/chat` }),
3739
[baseUrl],
3840
);
3941

4042
const { messages, sendMessage, setMessages, status, error } = useChat({
4143
transport,
42-
messages: loadMessages() as UIMessage[],
44+
messages: initialMessages,
4345
});
4446

4547
const isStreaming = status === 'streaming' || status === 'submitted';

apps/studio/src/hooks/use-ai-chat-panel.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,28 @@ export function useAiChatPanel() {
6262
}, []);
6363

6464
const toggle = useCallback(() => {
65-
setOpen(!isOpen);
66-
}, [isOpen, setOpen]);
65+
setIsOpen((prev) => {
66+
const next = !prev;
67+
try {
68+
localStorage.setItem(PANEL_STATE_KEY, String(next));
69+
} catch {
70+
// silently ignore
71+
}
72+
return next;
73+
});
74+
}, []);
6775

6876
// Global keyboard shortcut: Ctrl+Shift+I / Cmd+Shift+I
6977
useEffect(() => {
7078
function handleKeyDown(e: KeyboardEvent) {
7179
if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key === 'I') {
7280
e.preventDefault();
73-
setOpen(!isOpen);
81+
toggle();
7482
}
7583
}
7684
window.addEventListener('keydown', handleKeyDown);
7785
return () => window.removeEventListener('keydown', handleKeyDown);
78-
}, [isOpen, setOpen]);
86+
}, [toggle]);
7987

8088
return { isOpen, setOpen, toggle };
8189
}

apps/studio/test/ai-chat-panel.test.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
33

44
import { describe, it, expect, beforeEach } from 'vitest';
5+
import type { UIMessage } from 'ai';
56
import {
67
loadMessages,
78
saveMessages,
89
} from '../src/hooks/use-ai-chat-panel';
910

10-
function makeMsg(overrides: { id: string; role: 'user' | 'assistant'; content: string }) {
11+
function makeMsg(overrides: { id: string; role: 'user' | 'assistant'; content: string }): UIMessage {
1112
return {
1213
id: overrides.id,
1314
role: overrides.role,
@@ -48,15 +49,15 @@ describe('use-ai-chat-panel', () => {
4849
describe('saveMessages', () => {
4950
it('persists messages to localStorage', () => {
5051
const msgs = [makeMsg({ id: '1', role: 'user', content: 'Hello' })];
51-
saveMessages(msgs as any);
52+
saveMessages(msgs);
5253
const stored = JSON.parse(localStorage.getItem('objectstack:ai-chat-messages') || '[]');
5354
expect(stored).toHaveLength(1);
5455
expect(stored[0].parts[0].text).toBe('Hello');
5556
});
5657

5758
it('overwrites previous messages', () => {
58-
saveMessages([makeMsg({ id: '1', role: 'user', content: 'A' })] as any);
59-
saveMessages([makeMsg({ id: '2', role: 'user', content: 'B' })] as any);
59+
saveMessages([makeMsg({ id: '1', role: 'user', content: 'A' })]);
60+
saveMessages([makeMsg({ id: '2', role: 'user', content: 'B' })]);
6061
const stored = JSON.parse(localStorage.getItem('objectstack:ai-chat-messages') || '[]');
6162
expect(stored).toHaveLength(1);
6263
expect(stored[0].parts[0].text).toBe('B');
@@ -65,7 +66,7 @@ describe('use-ai-chat-panel', () => {
6566
it('does not throw when localStorage is unavailable', () => {
6667
const originalSetItem = Storage.prototype.setItem;
6768
Storage.prototype.setItem = () => { throw new Error('QuotaExceeded'); };
68-
expect(() => saveMessages([makeMsg({ id: '1', role: 'user', content: 'A' })] as any)).not.toThrow();
69+
expect(() => saveMessages([makeMsg({ id: '1', role: 'user', content: 'A' })])).not.toThrow();
6970
Storage.prototype.setItem = originalSetItem;
7071
});
7172
});
@@ -89,7 +90,7 @@ describe('AiChatPanel constants', () => {
8990
it('uses correct localStorage keys', () => {
9091
// Validate the keys used by the module match expectations
9192
const msgs = [makeMsg({ id: '1', role: 'user', content: 'test' })];
92-
saveMessages(msgs as any);
93+
saveMessages(msgs);
9394
expect(localStorage.getItem('objectstack:ai-chat-messages')).toBeTruthy();
9495
});
9596
});

0 commit comments

Comments
 (0)