Skip to content

Commit 1d02b61

Browse files
committed
Rename Callbacks tab to Lifecycle, improve text readability
PR review feedback: "Callbacks" was too vague as a tab name, and the zinc-grey text was hard to read against the dark background. - Rename tab from "Callbacks" to "Lifecycle" throughout - Brighten section headers and timestamps from zinc-600 to zinc-400 - Brighten empty-state text from zinc-700 to zinc-500 - Use indigo-300 for callback summary text
1 parent f7660ac commit 1d02b61

File tree

6 files changed

+132
-59
lines changed

6 files changed

+132
-59
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
ably-common/
22
specification/
3+
.next/

demo/vercel/react/use-chat/src/app/chat.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,26 +39,32 @@ export function Chat({ chatId, clientId, historyLimit }: { chatId: string; clien
3939
id: chatId,
4040
transport: chatTransport,
4141
onToolCall: ({ toolCall }) => {
42-
setCallbackLog(prev => [...prev, {
43-
time: Date.now(),
44-
type: 'onToolCall',
45-
summary: `${toolCall.toolName}(${JSON.stringify(toolCall.input)})`,
46-
}]);
42+
setCallbackLog((prev) => [
43+
...prev,
44+
{
45+
time: Date.now(),
46+
type: 'onToolCall',
47+
summary: `${toolCall.toolName}(${JSON.stringify(toolCall.input)})`,
48+
},
49+
]);
4750
},
4851
onFinish: ({ message, finishReason }) => {
49-
setCallbackLog(prev => [...prev, {
50-
time: Date.now(),
51-
type: 'onFinish',
52-
summary: `reason=${String(finishReason)}, parts=${String(message.parts.length)}`,
53-
}]);
52+
setCallbackLog((prev) => [
53+
...prev,
54+
{
55+
time: Date.now(),
56+
type: 'onFinish',
57+
summary: `reason=${String(finishReason)}, parts=${String(message.parts.length)}`,
58+
},
59+
]);
5460
},
5561
});
5662

5763
useMessageSync(transport, setMessages, chatTransport);
5864

5965
// Track status transitions
6066
useEffect(() => {
61-
setStatusLog(prev => [...prev, { time: Date.now(), status }]);
67+
setStatusLog((prev) => [...prev, { time: Date.now(), status }]);
6268
}, [status]);
6369

6470
const activeTurns = useActiveTurns({ transport });

demo/vercel/react/use-chat/src/app/components/debug-pane.tsx

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface DebugPaneProps {
2020
onClearLogs: () => void;
2121
}
2222

23-
type Tab = 'ably' | 'uimessages' | 'callbacks';
23+
type Tab = 'ably' | 'uimessages' | 'lifecycle';
2424

2525
function extractHeaders(msg: Ably.InboundMessage): Record<string, string> {
2626
const extras = msg.extras as { headers?: Record<string, string> } | undefined;
@@ -159,7 +159,7 @@ const statusColors: Record<string, string> = {
159159
error: 'text-red-400',
160160
};
161161

162-
function CallbacksTab({
162+
function LifecycleTab({
163163
callbackLog,
164164
statusLog,
165165
onClear,
@@ -177,9 +177,12 @@ function CallbacksTab({
177177
}, [callbackLog, statusLog]);
178178

179179
return (
180-
<div ref={scrollRef} className="flex-1 overflow-y-auto p-3 space-y-3">
180+
<div
181+
ref={scrollRef}
182+
className="flex-1 overflow-y-auto p-3 space-y-3"
183+
>
181184
<div className="flex items-center justify-between mb-2">
182-
<span className="text-[10px] text-zinc-600 uppercase tracking-wider">Status transitions</span>
185+
<span className="text-[10px] text-zinc-400 uppercase tracking-wider">Status transitions</span>
183186
<button
184187
onClick={onClear}
185188
className="text-[10px] text-zinc-600 hover:text-zinc-400 transition-colors"
@@ -189,11 +192,14 @@ function CallbacksTab({
189192
</div>
190193

191194
{statusLog.length === 0 ? (
192-
<p className="text-xs text-zinc-700 text-center">No status changes yet.</p>
195+
<p className="text-xs text-zinc-500 text-center">No status changes yet.</p>
193196
) : (
194197
<div className="rounded border border-zinc-800 bg-zinc-900/50 p-2 text-[11px] font-mono flex flex-wrap gap-1 items-center">
195198
{statusLog.map((entry, idx) => (
196-
<span key={idx} className="flex items-center gap-1">
199+
<span
200+
key={idx}
201+
className="flex items-center gap-1"
202+
>
197203
{idx > 0 && <span className="text-zinc-700">&rarr;</span>}
198204
<span className={statusColors[entry.status] ?? 'text-zinc-500'}>{entry.status}</span>
199205
</span>
@@ -202,38 +208,38 @@ function CallbacksTab({
202208
)}
203209

204210
<div className="mt-4 mb-2">
205-
<span className="text-[10px] text-zinc-600 uppercase tracking-wider">Callbacks</span>
211+
<span className="text-[10px] text-zinc-400 uppercase tracking-wider">Callbacks</span>
206212
</div>
207213

208214
{callbackLog.length === 0 ? (
209-
<p className="text-xs text-zinc-700 text-center">
210-
Callbacks (onToolCall, onFinish) will appear here.
211-
</p>
215+
<p className="text-xs text-zinc-500 text-center">Callbacks (onToolCall, onFinish) will appear here.</p>
212216
) : (
213217
callbackLog.map((entry, idx) => (
214218
<div
215219
key={idx}
216220
className="rounded border border-zinc-800 bg-zinc-900/50 p-2 text-[11px] font-mono"
217221
>
218222
<div className="flex items-center gap-2 mb-1">
219-
<span className="text-zinc-600">
220-
{new Date(entry.time).toLocaleTimeString()}
221-
</span>
222-
<span className={callbackTypeColors[entry.type] ?? 'text-zinc-400'}>
223-
{entry.type}
224-
</span>
225-
</div>
226-
<div className="text-zinc-500 break-all whitespace-pre-wrap">
227-
{entry.summary}
223+
<span className="text-zinc-400">{new Date(entry.time).toLocaleTimeString()}</span>
224+
<span className={callbackTypeColors[entry.type] ?? 'text-zinc-400'}>{entry.type}</span>
228225
</div>
226+
<div className="text-indigo-300 break-all whitespace-pre-wrap">{entry.summary}</div>
229227
</div>
230228
))
231229
)}
232230
</div>
233231
);
234232
}
235233

236-
export function DebugPane({ messages, ablyMessages, activeTurns, status, callbackLog, statusLog, onClearLogs }: DebugPaneProps) {
234+
export function DebugPane({
235+
messages,
236+
ablyMessages,
237+
activeTurns,
238+
status,
239+
callbackLog,
240+
statusLog,
241+
onClearLogs,
242+
}: DebugPaneProps) {
237243
const [isOpen, setIsOpen] = useState(false);
238244
const [tab, setTab] = useState<Tab>('ably');
239245

@@ -272,12 +278,12 @@ export function DebugPane({ messages, ablyMessages, activeTurns, status, callbac
272278
<span className="ml-1 text-zinc-600">{messages.length}</span>
273279
</button>
274280
<button
275-
onClick={() => setTab('callbacks')}
281+
onClick={() => setTab('lifecycle')}
276282
className={`text-[10px] px-2 py-1 rounded transition-colors ${
277-
tab === 'callbacks' ? 'bg-zinc-800 text-zinc-300' : 'text-zinc-600 hover:text-zinc-400'
283+
tab === 'lifecycle' ? 'bg-zinc-800 text-zinc-300' : 'text-zinc-600 hover:text-zinc-400'
278284
}`}
279285
>
280-
Callbacks
286+
Lifecycle
281287
<span className="ml-1 text-zinc-600">{callbackLog.length}</span>
282288
</button>
283289
</div>
@@ -297,7 +303,7 @@ export function DebugPane({ messages, ablyMessages, activeTurns, status, callbac
297303
status={status}
298304
/>
299305
) : (
300-
<CallbacksTab
306+
<LifecycleTab
301307
callbackLog={callbackLog}
302308
statusLog={statusLog}
303309
onClear={onClearLogs}

test/vercel/react/use-message-sync.test.ts

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ const createMockTransport = (): MockTransport => {
7474
return { transport, emitView, viewFlattenNodes };
7575
};
7676

77-
/** Create a mock ChatTransport with controllable streaming state. */
77+
/**
78+
* Create a mock ChatTransport with controllable streaming state.
79+
* @returns Mock chat transport and a function to toggle the streaming flag.
80+
*/
7881
const createMockChatTransport = (): {
7982
chatTransport: ChatTransport;
8083
setStreaming: (value: boolean) => void;
@@ -92,10 +95,14 @@ const createMockChatTransport = (): {
9295
// eslint-disable-next-line unicorn/no-null -- required by AI SDK ChatTransport contract
9396
reconnectToStream: vi.fn().mockResolvedValue(null),
9497
close: vi.fn(),
95-
get streaming() { return streaming; },
98+
get streaming() {
99+
return streaming;
100+
},
96101
onStreamingChange: (cb: (s: boolean) => void) => {
97102
callbacks.add(cb);
98-
return () => { callbacks.delete(cb); };
103+
return () => {
104+
callbacks.delete(cb);
105+
};
99106
},
100107
} as unknown as ChatTransport;
101108

@@ -165,36 +172,66 @@ describe('useMessageSync', () => {
165172
const mock = createMockTransport();
166173
const { chatTransport, setStreaming } = createMockChatTransport();
167174
const msgs = [makeMessage('1')];
168-
mock.viewFlattenNodes.mockReturnValue(msgs.map((m) => ({ message: m, msgId: m.id, parentId: undefined, forkOf: undefined, headers: {}, serial: undefined })));
175+
mock.viewFlattenNodes.mockReturnValue(
176+
msgs.map((m) => ({
177+
message: m,
178+
msgId: m.id,
179+
parentId: undefined,
180+
forkOf: undefined,
181+
headers: {},
182+
serial: undefined,
183+
})),
184+
);
169185

170186
const setMessages = vi.fn();
171-
renderHook(() => { useMessageSync(mock.transport, setMessages, chatTransport); });
187+
renderHook(() => {
188+
useMessageSync(mock.transport, setMessages, chatTransport);
189+
});
172190

173191
// Initial sync fires on mount (not yet streaming)
174192
expect(setMessages).toHaveBeenCalledTimes(1);
175193
setMessages.mockClear();
176194

177195
// Start streaming — gate closes
178-
act(() => { setStreaming(true); });
196+
act(() => {
197+
setStreaming(true);
198+
});
179199

180200
// View updates should be suppressed
181-
act(() => { mock.emitView('update'); });
201+
act(() => {
202+
mock.emitView('update');
203+
});
182204
expect(setMessages).not.toHaveBeenCalled();
183205
});
184206

185207
it('syncs immediately when streaming ends', () => {
186208
const mock = createMockTransport();
187209
const { chatTransport, setStreaming } = createMockChatTransport();
188210
const msgs = [makeMessage('1'), makeMessage('2', 'assistant')];
189-
mock.viewFlattenNodes.mockReturnValue(msgs.map((m) => ({ message: m, msgId: m.id, parentId: undefined, forkOf: undefined, headers: {}, serial: undefined })));
211+
mock.viewFlattenNodes.mockReturnValue(
212+
msgs.map((m) => ({
213+
message: m,
214+
msgId: m.id,
215+
parentId: undefined,
216+
forkOf: undefined,
217+
headers: {},
218+
serial: undefined,
219+
})),
220+
);
190221

191222
const setMessages = vi.fn();
192-
renderHook(() => { useMessageSync(mock.transport, setMessages, chatTransport); });
223+
renderHook(() => {
224+
useMessageSync(mock.transport, setMessages, chatTransport);
225+
});
193226
setMessages.mockClear();
194227

195228
// Gate: streaming on then off
196-
act(() => { setStreaming(true); });
197-
act(() => { setStreaming(false); });
229+
act(() => {
230+
setStreaming(true);
231+
});
232+
act(() => {
233+
setStreaming(false);
234+
});
198235

199236
// Immediate sync on gate open
200237
expect(setMessages).toHaveBeenCalledTimes(1);
@@ -205,10 +242,14 @@ describe('useMessageSync', () => {
205242
it('works without chatTransport (no gating)', () => {
206243
const mock = createMockTransport();
207244
const setMessages = vi.fn();
208-
renderHook(() => { useMessageSync(mock.transport, setMessages); });
245+
renderHook(() => {
246+
useMessageSync(mock.transport, setMessages);
247+
});
209248

210249
// Initial sync + view update both work
211-
act(() => { mock.emitView('update'); });
250+
act(() => {
251+
mock.emitView('update');
252+
});
212253
expect(setMessages).toHaveBeenCalledTimes(2);
213254
});
214255

@@ -223,7 +264,9 @@ describe('useMessageSync', () => {
223264
]);
224265

225266
const setMessages = vi.fn();
226-
renderHook(() => { useMessageSync(mock.transport, setMessages, chatTransport); });
267+
renderHook(() => {
268+
useMessageSync(mock.transport, setMessages, chatTransport);
269+
});
227270

228271
// Initial sync: just the user message
229272
expect(setMessages).toHaveBeenCalledTimes(1);
@@ -232,7 +275,9 @@ describe('useMessageSync', () => {
232275
setMessages.mockClear();
233276

234277
// Own-turn stream starts — gate closes
235-
act(() => { setStreaming(true); });
278+
act(() => {
279+
setStreaming(true);
280+
});
236281

237282
// Observer message arrives while gated (another user's assistant response).
238283
// The transport tree has it, but setMessages should NOT fire.
@@ -241,11 +286,15 @@ describe('useMessageSync', () => {
241286
{ message: userMsg, msgId: '1', parentId: undefined, forkOf: undefined, headers: {}, serial: undefined },
242287
{ message: observerMsg, msgId: 'observer-1', parentId: '1', forkOf: undefined, headers: {}, serial: undefined },
243288
]);
244-
act(() => { mock.emitView('update'); });
289+
act(() => {
290+
mock.emitView('update');
291+
});
245292
expect(setMessages).not.toHaveBeenCalled();
246293

247294
// Own-turn stream ends — gate opens, immediate sync picks up observer message
248-
act(() => { setStreaming(false); });
295+
act(() => {
296+
setStreaming(false);
297+
});
249298
expect(setMessages).toHaveBeenCalledTimes(1);
250299
const gateOpenUpdater = setMessages.mock.calls[0]?.[0] as (prev: AI.UIMessage[]) => AI.UIMessage[];
251300
expect(gateOpenUpdater([])).toEqual([userMsg, observerMsg]);

test/vercel/transport/chat-transport-usechat.test.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,11 @@ const createMultiTurnMockTransport = () => {
184184
* @param textId
185185
* @param deltas
186186
*/
187-
/** Extract the concatenated text from an assistant message's parts. */
187+
/**
188+
* Extract the concatenated text from an assistant message's parts.
189+
* @param msg - The assistant message to extract text from.
190+
* @returns Concatenated text content.
191+
*/
188192
const getAssistantText = (msg: AI.UIMessage): string =>
189193
msg.parts
190194
.filter((p): p is AI.TextUIPart => p.type === 'text')
@@ -386,7 +390,6 @@ describe('ChatTransport useChat integration — features work with the real stre
386390
// -------------------------------------------------------------------------
387391

388392
describe('multiple streaming responses', () => {
389-
390393
it('sequential: two responses produce four correctly ordered messages', async () => {
391394
const { transport, turnA, turnB } = createMultiTurnMockTransport();
392395
const chatTransport = createChatTransport(transport);
@@ -428,9 +431,13 @@ describe('ChatTransport useChat integration — features work with the real stre
428431
expect(msgs[3]?.role).toBe('assistant');
429432

430433
expect(msgs[1]?.id).toBe('assistant-a');
431-
expect(getAssistantText(msgs[1] ?? { id: '', role: 'assistant', parts: [] } as AI.UIMessage)).toBe('Response A.');
434+
expect(getAssistantText(msgs[1] ?? ({ id: '', role: 'assistant', parts: [] } as AI.UIMessage))).toBe(
435+
'Response A.',
436+
);
432437
expect(msgs[3]?.id).toBe('assistant-b');
433-
expect(getAssistantText(msgs[3] ?? { id: '', role: 'assistant', parts: [] } as AI.UIMessage)).toBe('Response B.');
438+
expect(getAssistantText(msgs[3] ?? ({ id: '', role: 'assistant', parts: [] } as AI.UIMessage))).toBe(
439+
'Response B.',
440+
);
434441

435442
// onFinish fires twice with the correct messages
436443
expect(onFinish).toHaveBeenCalledTimes(2);
@@ -486,8 +493,12 @@ describe('ChatTransport useChat integration — features work with the real stre
486493
expect(msgs.map((m) => m.role)).toEqual(['user', 'user', 'assistant', 'assistant']);
487494

488495
// Content correct for both responses.
489-
expect(getAssistantText(msgs[2] ?? { id: '', role: 'assistant', parts: [] } as AI.UIMessage)).toBe('Response A.');
490-
expect(getAssistantText(msgs[3] ?? { id: '', role: 'assistant', parts: [] } as AI.UIMessage)).toBe('Response B.');
496+
expect(getAssistantText(msgs[2] ?? ({ id: '', role: 'assistant', parts: [] } as AI.UIMessage))).toBe(
497+
'Response A.',
498+
);
499+
expect(getAssistantText(msgs[3] ?? ({ id: '', role: 'assistant', parts: [] } as AI.UIMessage))).toBe(
500+
'Response B.',
501+
);
491502

492503
// onFinish still fires once — the activeResponse overwrite happens
493504
// before sendMessages, so our queue can't prevent it.

0 commit comments

Comments
 (0)