Skip to content

Commit b6603bb

Browse files
authored
Merge pull request #431 from Mng-dev-ai/improve/frontend-comments-and-renames
Improve frontend naming and add contextual comments
2 parents 4b68fc2 + 076e895 commit b6603bb

21 files changed

Lines changed: 234 additions & 91 deletions

frontend/src/hooks/useActiveViews.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ function arraysEqual(a: ViewType[], b: ViewType[]): boolean {
1111
return true;
1212
}
1313

14+
// Derives the list of visible view panes from the mosaic layout. Uses a ref
15+
// to return a referentially stable array when the leaf set hasn't changed,
16+
// preventing downstream re-renders from Zustand selector identity changes.
1417
export function useActiveViews(): ViewType[] {
1518
const prevRef = useRef<ViewType[]>([]);
1619

frontend/src/hooks/useChatStreaming.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ function findActiveStreamForChat(chatId: string) {
7272
return undefined;
7373
}
7474

75+
// Top-level hook that wires together the streaming pipeline for a single chat view.
76+
// Composes useStreamCallbacks (envelope processing), useStreamReconnect (resume on
77+
// navigation), useMessageActions (send/stop), and useInputState (draft persistence).
78+
// Returns the full set of state and handlers consumed by the chat UI components.
7579
export function useChatStreaming({
7680
chatId,
7781
currentChat,
@@ -152,10 +156,13 @@ export function useChatStreaming({
152156
onPendingUserMessageIdChange: setPendingUserMessageIdState,
153157
});
154158

159+
// Keep the global stream store's callbacks in sync with the latest hook closures.
160+
// Streams outlive individual renders, so without this the store would dispatch
161+
// events through stale callbacks that close over outdated chatId/messages.
155162
useEffect(() => {
156163
if (!chatId) return;
157164

158-
const updateCallbacks = () => {
165+
const syncCallbacksToStore = () => {
159166
const existingStream = findActiveStreamForChat(chatId);
160167
if (existingStream) {
161168
useStreamStore.getState().updateStreamCallbacks(chatId, existingStream.messageId, {
@@ -167,13 +174,13 @@ export function useChatStreaming({
167174
}
168175
};
169176

170-
updateCallbacks();
177+
syncCallbacksToStore();
171178

172179
let prevStreams = useStreamStore.getState().activeStreams;
173180
const unsubscribe = useStreamStore.subscribe((state) => {
174181
if (state.activeStreams !== prevStreams) {
175182
prevStreams = state.activeStreams;
176-
updateCallbacks();
183+
syncCallbacksToStore();
177184
}
178185
});
179186
return () => unsubscribe();
@@ -189,10 +196,13 @@ export function useChatStreaming({
189196
setMessages([]);
190197
}
191198

199+
// Subscribes to the stream store and mirrors active-stream presence into
200+
// local React state (streamState, currentMessageId). This is the bridge
201+
// between the global EventSource lifecycle and the per-chat UI indicators.
192202
useEffect(() => {
193203
if (!chatId) return;
194204

195-
const syncStreamState = () => {
205+
const reconcileStreamState = () => {
196206
const activeStreamForChat = findActiveStreamForChat(chatId);
197207

198208
if (activeStreamForChat) {
@@ -215,9 +225,9 @@ export function useChatStreaming({
215225
}
216226
};
217227

218-
syncStreamState();
228+
reconcileStreamState();
219229

220-
const unsubscribe = useStreamStore.subscribe(syncStreamState);
230+
const unsubscribe = useStreamStore.subscribe(reconcileStreamState);
221231
return () => unsubscribe();
222232
}, [chatId]);
223233

@@ -261,7 +271,10 @@ export function useChatStreaming({
261271
replayStream,
262272
});
263273

264-
const handleStopStream = useCallback(
274+
// Sends stop requests for one or all active streams in the current chat.
275+
// Immediately marks the UI as idle (optimistic) and tracks pending stops
276+
// so incoming envelopes for the stopping stream are ignored.
277+
const stopActiveStreams = useCallback(
265278
async (messageId?: string) => {
266279
const pendingIds = new Set<string>();
267280
const stopPromises: Promise<void>[] = [];
@@ -306,9 +319,9 @@ export function useChatStreaming({
306319
}, [currentMessageId]);
307320

308321
const handleStop = useCallback(() => {
309-
void handleStopStream(currentMessageIdRef.current || undefined);
322+
void stopActiveStreams(currentMessageIdRef.current || undefined);
310323
clearInput();
311-
}, [handleStopStream, clearInput]);
324+
}, [stopActiveStreams, clearInput]);
312325

313326
useMountEffect(() => {
314327
cleanupExpiredPdfBlobs();

frontend/src/hooks/useContextUsageState.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ interface UseContextUsageStateResult {
1313
updateContextUsage: (data: ContextUsage, chatId?: string) => void;
1414
}
1515

16+
// Three sources feed token usage, each arriving at different times:
17+
// (1) the chat object's cached `context_token_usage` for instant display on
18+
// navigation, (2) the dedicated context-usage query for accurate numbers
19+
// after load, and (3) live SSE system envelopes during streaming via
20+
// `updateContextUsage`. Whichever writes last wins.
1621
export function useContextUsageState(
1722
chatId: string | undefined,
1823
currentChat: Chat | undefined,
@@ -52,6 +57,8 @@ export function useContextUsageState(
5257
setTokensUsed(contextUsageData.tokens_used ?? 0);
5358
}, [chatId, contextUsageData]);
5459

60+
// Called from SSE system envelopes during streaming. Ignores updates for
61+
// off-screen chats so token counts don't bleed across chat switches.
5562
const updateContextUsage = useCallback((data: ContextUsage, incomingChatId?: string) => {
5663
if (incomingChatId && incomingChatId !== currentChatIdRef.current) {
5764
return;

frontend/src/hooks/useDragAndDrop.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ interface UseDragAndDropOptions {
77

88
export function useDragAndDrop({ onFilesDrop, disabled = false }: UseDragAndDropOptions = {}) {
99
const [isDragging, setIsDragging] = useState(false);
10+
// Tracks enter/leave balance across child elements — dragenter/dragleave
11+
// fire for every descendant, so a simple boolean would flicker. Only reset
12+
// isDragging when the counter reaches 0 (cursor left the drop zone entirely).
1013
const dragCounter = useRef(0);
1114

1215
const handleDragIn = useCallback(

frontend/src/hooks/useGlobalStream.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,48 @@ import { chatService } from '@/services/chatService';
66

77
interface UseGlobalStreamOptions {
88
enabled?: boolean;
9-
onValidationComplete?: () => void;
9+
onPruneComplete?: () => void;
1010
}
1111

12+
// On app mount, reconciles the in-memory stream store with the server — streams
13+
// tracked locally (e.g., across a page refresh) but no longer active on the
14+
// backend get pruned so the UI doesn't show stale "streaming" indicators.
1215
export function useGlobalStream(options?: UseGlobalStreamOptions) {
13-
const hasValidatedRef = useRef(false);
16+
const hasPrunedRef = useRef(false);
1417
const enabled = options?.enabled ?? true;
15-
const onValidationComplete = options?.onValidationComplete;
18+
const onPruneComplete = options?.onPruneComplete;
1619

1720
useEffect(() => {
1821
if (!enabled) return;
19-
if (hasValidatedRef.current) return;
20-
hasValidatedRef.current = true;
22+
if (hasPrunedRef.current) return;
23+
hasPrunedRef.current = true;
2124

22-
const validateStreams = async () => {
25+
const pruneStaleStreams = async () => {
2326
const metadata = useStreamStore.getState().activeStreamMetadata;
2427

2528
if (metadata.length === 0) return;
2629

27-
const validationPromises = metadata.map(async (streamMeta) => {
30+
const prunePromises = metadata.map(async (streamMeta) => {
2831
try {
2932
const status = await chatService.checkChatStatus(streamMeta.chatId);
3033

3134
if (!status?.has_active_task) {
3235
useStreamStore.getState().removeStreamMetadata(streamMeta.chatId);
3336
}
3437
} catch (error) {
35-
logger.error('Stream validation failed', 'useGlobalStream', error);
38+
logger.error('Stream prune check failed', 'useGlobalStream', error);
3639
useStreamStore.getState().removeStreamMetadata(streamMeta.chatId);
3740
}
3841
});
3942

40-
await Promise.allSettled(validationPromises);
41-
onValidationComplete?.();
43+
await Promise.allSettled(prunePromises);
44+
onPruneComplete?.();
4245
};
4346

44-
const timeoutId = setTimeout(validateStreams, 500);
47+
const timeoutId = setTimeout(pruneStaleStreams, 500);
4548

4649
return () => clearTimeout(timeoutId);
47-
}, [enabled, onValidationComplete]);
50+
}, [enabled, onPruneComplete]);
4851

4952
const stopAllStreams = useCallback(async () => {
5053
await streamService.stopAllStreams();

frontend/src/hooks/useMessageActions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,17 @@ interface UseMessageActionsParams {
3434
isStreaming: boolean;
3535
}
3636

37+
// Detects a leftover empty assistant message (e.g., from a previous failed
38+
// stream start) so sendMessage can replace it rather than stacking duplicates.
3739
const isEmptyBotPlaceholder = (msg?: Message) =>
3840
!!msg?.is_bot &&
3941
(!msg?.content_render?.events || msg.content_render.events.length === 0) &&
4042
!msg.content_text;
4143

44+
// Exposes two layers: `sendMessage` opens the SSE stream and injects the
45+
// assistant placeholder into state; `handleMessageSend` is the form-level
46+
// wrapper that validates input size, creates the optimistic user message,
47+
// and rolls it back on failure.
4248
export function useMessageActions({
4349
chatId,
4450
selectedModelId,
@@ -120,6 +126,7 @@ export function useMessageActions({
120126
model_id: selectedModelId ?? undefined,
121127
};
122128

129+
// Replace a trailing empty placeholder if present, otherwise append.
123130
setMessages((prev) => {
124131
const lastMessage = prev[prev.length - 1];
125132
if (isEmptyBotPlaceholder(lastMessage)) {

frontend/src/hooks/useMessageCache.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ interface UseMessageCacheParams {
99
queryClient: QueryClient;
1010
}
1111

12+
// Direct query-cache mutators for messages. These write into the react-query
13+
// infinite-query pages structure (pages[].items[]) so that optimistic UI updates
14+
// (streaming content, new messages, deletions) are visible without a refetch.
15+
// Scoped to the current chatId — callers needing cross-chat writes must use
16+
// queryClient directly with the target chatId.
1217
export function useMessageCache({ chatId, queryClient }: UseMessageCacheParams) {
1318
const updateMessageInCache = useCallback(
1419
(messageId: string, updater: (msg: Message) => Message) => {
@@ -31,6 +36,8 @@ export function useMessageCache({ chatId, queryClient }: UseMessageCacheParams)
3136
[chatId, queryClient],
3237
);
3338

39+
// Uses unshift into page 0 so newest messages match the backend's DESC
40+
// ordering. Optionally prepends the paired user message when both arrive together.
3441
const addMessageToCache = useCallback(
3542
(message: Message, userMessage?: Message) => {
3643
if (!chatId) return;

frontend/src/hooks/useMessageInitialization.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ interface UseMessageInitializationParams {
1818
setInitialPrompt: (prompt: string) => void;
1919
}
2020

21+
// Normalizes fetched messages (attachment file types, default fields) and
22+
// seeds local state. Also handles the "initial prompt" flow where a new chat
23+
// is created from a route param or navigation state — injects a synthetic
24+
// user message so the chat starts without waiting for the first API round-trip.
2125
export function useMessageInitialization({
2226
fetchedMessages,
2327
chatId,
@@ -40,7 +44,7 @@ export function useMessageInitialization({
4044
// but always allow initialization when switching to a different chat
4145
if (isStreaming && initializedChatRef.current === chatId) return;
4246

43-
const formattedMessages = fetchedMessages.map((msg: Message) => {
47+
const normalizedMessages = fetchedMessages.map((msg: Message) => {
4448
const processedAttachments = msg.attachments?.map((attachment) => {
4549
const fileType = detectFileType(
4650
attachment.filename || '',
@@ -66,7 +70,9 @@ export function useMessageInitialization({
6670
};
6771
});
6872

69-
const latestKnownSeq = formattedMessages.reduce((maxSeq, message) => {
73+
// Persist the highest seq from fetched messages so stream reconnection
74+
// (useStreamReconnect) can resume from the correct cursor on page refresh.
75+
const latestKnownSeq = normalizedMessages.reduce((maxSeq, message) => {
7076
const seq = Number(message.last_seq ?? 0);
7177
return Number.isFinite(seq) && seq > maxSeq ? seq : maxSeq;
7278
}, 0);
@@ -76,7 +82,7 @@ export function useMessageInitialization({
7682

7783
if (
7884
initialPromptFromRoute &&
79-
formattedMessages.length === 0 &&
85+
normalizedMessages.length === 0 &&
8086
!initialPromptSent &&
8187
selectedModelId
8288
) {
@@ -89,9 +95,9 @@ export function useMessageInitialization({
8995
);
9096
setMessages([initialMessage]);
9197
setInitialPrompt(initialPromptFromRoute);
92-
} else if (formattedMessages.length > 0) {
98+
} else if (normalizedMessages.length > 0) {
9399
initializedChatRef.current = chatId;
94-
setMessages(formattedMessages);
100+
setMessages(normalizedMessages);
95101
}
96102
}, [
97103
fetchedMessages,

frontend/src/hooks/usePermissionRequest.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ function isExpiredRequestError(error: unknown): boolean {
2121
return (error as ApiError)?.status === 404;
2222
}
2323

24+
// Manages the tool-permission approval flow for a single chat. Reads the
25+
// pending request from the global permission store, sends approve/reject
26+
// responses to the backend, and auto-dismisses 404s (expired requests where
27+
// the backend already timed out or the stream moved on).
2428
export function usePermissionRequest(chatId: string | undefined): UsePermissionRequestReturn {
2529
const [isLoading, setIsLoading] = useState(false);
2630
const [error, setError] = useState<string | null>(null);
@@ -31,6 +35,8 @@ export function usePermissionRequest(chatId: string | undefined): UsePermissionR
3135
const pendingRequest = chatId ? (pendingRequests.get(chatId) ?? null) : null;
3236
const prevRequestIdRef = useRef(pendingRequest?.request_id);
3337

38+
// Clear stale error when a new permission request arrives, so errors from
39+
// a previous request don't bleed into the new approval dialog.
3440
if (prevRequestIdRef.current !== pendingRequest?.request_id) {
3541
prevRequestIdRef.current = pendingRequest?.request_id;
3642
if (error !== null) setError(null);

0 commit comments

Comments
 (0)