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
67 changes: 38 additions & 29 deletions src/features/app/hooks/useAppServerEvents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import type {
AppServerEvent,
ApprovalRequest,
Expand Down Expand Up @@ -118,9 +118,18 @@ export const METHODS_ROUTED_IN_USE_APP_SERVER_EVENTS = [
] as const satisfies readonly SupportedAppServerMethod[];

export function useAppServerEvents(handlers: AppServerEventHandlers) {
// Use ref to keep handlers current without triggering re-subscription
const handlersRef = useRef(handlers);

// Update ref on every render to always have latest handlers
useEffect(() => {
handlersRef.current = handlers;
});

useEffect(() => {
const unlisten = subscribeAppServerEvents((payload) => {
handlers.onAppServerEvent?.(payload);
const currentHandlers = handlersRef.current;
currentHandlers.onAppServerEvent?.(payload);

const { workspace_id } = payload;
const method = getAppServerRawMethod(payload);
Expand All @@ -130,15 +139,15 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const params = getAppServerParams(payload);

if (method === "codex/connected") {
handlers.onWorkspaceConnected?.(workspace_id);
currentHandlers.onWorkspaceConnected?.(workspace_id);
return;
}

const requestId = getAppServerRequestId(payload);
const hasRequestId = requestId !== null;

if (isApprovalRequestMethod(method) && hasRequestId) {
handlers.onApprovalRequest?.({
currentHandlers.onApprovalRequest?.({
workspace_id,
request_id: requestId as string | number,
method,
Expand Down Expand Up @@ -177,7 +186,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
};
})
.filter((question) => question.id);
handlers.onRequestUserInput?.({
currentHandlers.onRequestUserInput?.({
workspace_id,
request_id: requestId as string | number,
params: {
Expand All @@ -195,7 +204,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const itemId = String(params.itemId ?? params.item_id ?? "");
const delta = String(params.delta ?? "");
if (threadId && itemId && delta) {
handlers.onAgentMessageDelta?.({
currentHandlers.onAgentMessageDelta?.({
workspaceId: workspace_id,
threadId,
itemId,
Expand All @@ -212,7 +221,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
);
const turnId = String(turn?.id ?? params.turnId ?? params.turn_id ?? "");
if (threadId) {
handlers.onTurnStarted?.(workspace_id, threadId, turnId);
currentHandlers.onTurnStarted?.(workspace_id, threadId, turnId);
}
return;
}
Expand All @@ -221,7 +230,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const thread = (params.thread as Record<string, unknown> | undefined) ?? null;
const threadId = String(thread?.id ?? "");
if (thread && threadId) {
handlers.onThreadStarted?.(workspace_id, thread);
currentHandlers.onThreadStarted?.(workspace_id, thread);
}
return;
}
Expand All @@ -234,7 +243,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
? threadNameRaw.trim()
: null;
if (threadId) {
handlers.onThreadNameUpdated?.(workspace_id, { threadId, threadName });
currentHandlers.onThreadNameUpdated?.(workspace_id, { threadId, threadName });
}
return;
}
Expand All @@ -243,7 +252,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const threadId = String(params.threadId ?? params.thread_id ?? "");
const action = String(params.action ?? "hide");
if (threadId) {
handlers.onBackgroundThreadAction?.(workspace_id, threadId, action);
currentHandlers.onBackgroundThreadAction?.(workspace_id, threadId, action);
}
return;
}
Expand All @@ -255,7 +264,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const messageText = String(error.message ?? "");
const willRetry = Boolean(params.willRetry ?? params.will_retry);
if (threadId) {
handlers.onTurnError?.(workspace_id, threadId, turnId, {
currentHandlers.onTurnError?.(workspace_id, threadId, turnId, {
message: messageText,
willRetry,
});
Expand All @@ -270,7 +279,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
);
const turnId = String(turn?.id ?? params.turnId ?? params.turn_id ?? "");
if (threadId) {
handlers.onTurnCompleted?.(workspace_id, threadId, turnId);
currentHandlers.onTurnCompleted?.(workspace_id, threadId, turnId);
}
return;
}
Expand All @@ -279,7 +288,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const threadId = String(params.threadId ?? params.thread_id ?? "");
const turnId = String(params.turnId ?? params.turn_id ?? "");
if (threadId) {
handlers.onTurnPlanUpdated?.(workspace_id, threadId, turnId, {
currentHandlers.onTurnPlanUpdated?.(workspace_id, threadId, turnId, {
explanation: params.explanation,
plan: params.plan,
});
Expand All @@ -291,7 +300,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const threadId = String(params.threadId ?? params.thread_id ?? "");
const diff = String(params.diff ?? "");
if (threadId && diff) {
handlers.onTurnDiffUpdated?.(workspace_id, threadId, diff);
currentHandlers.onTurnDiffUpdated?.(workspace_id, threadId, diff);
}
return;
}
Expand All @@ -302,7 +311,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
(params.tokenUsage as Record<string, unknown> | null | undefined) ??
(params.token_usage as Record<string, unknown> | null | undefined);
if (threadId && tokenUsage !== undefined) {
handlers.onThreadTokenUsageUpdated?.(workspace_id, threadId, tokenUsage);
currentHandlers.onThreadTokenUsageUpdated?.(workspace_id, threadId, tokenUsage);
}
return;
}
Expand All @@ -312,7 +321,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
(params.rateLimits as Record<string, unknown> | undefined) ??
(params.rate_limits as Record<string, unknown> | undefined);
if (rateLimits) {
handlers.onAccountRateLimitsUpdated?.(workspace_id, rateLimits);
currentHandlers.onAccountRateLimitsUpdated?.(workspace_id, rateLimits);
}
return;
}
Expand All @@ -323,7 +332,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
typeof authModeRaw === "string" && authModeRaw.trim().length > 0
? authModeRaw
: null;
handlers.onAccountUpdated?.(workspace_id, authMode);
currentHandlers.onAccountUpdated?.(workspace_id, authMode);
return;
}

Expand All @@ -337,7 +346,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const errorRaw = params.error ?? null;
const error =
typeof errorRaw === "string" && errorRaw.trim().length > 0 ? errorRaw : null;
handlers.onAccountLoginCompleted?.(workspace_id, {
currentHandlers.onAccountLoginCompleted?.(workspace_id, {
loginId,
success,
error,
Expand All @@ -349,13 +358,13 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const threadId = String(params.threadId ?? params.thread_id ?? "");
const item = params.item as Record<string, unknown> | undefined;
if (threadId && item) {
handlers.onItemCompleted?.(workspace_id, threadId, item);
currentHandlers.onItemCompleted?.(workspace_id, threadId, item);
}
if (threadId && item?.type === "agentMessage") {
const itemId = String(item.id ?? "");
const text = String(item.text ?? "");
if (itemId) {
handlers.onAgentMessageCompleted?.({
currentHandlers.onAgentMessageCompleted?.({
workspaceId: workspace_id,
threadId,
itemId,
Expand All @@ -370,7 +379,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const threadId = String(params.threadId ?? params.thread_id ?? "");
const item = params.item as Record<string, unknown> | undefined;
if (threadId && item) {
handlers.onItemStarted?.(workspace_id, threadId, item);
currentHandlers.onItemStarted?.(workspace_id, threadId, item);
}
return;
}
Expand All @@ -380,7 +389,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const itemId = String(params.itemId ?? params.item_id ?? "");
const delta = String(params.delta ?? "");
if (threadId && itemId && delta) {
handlers.onReasoningSummaryDelta?.(workspace_id, threadId, itemId, delta);
currentHandlers.onReasoningSummaryDelta?.(workspace_id, threadId, itemId, delta);
}
return;
}
Expand All @@ -389,7 +398,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const threadId = String(params.threadId ?? params.thread_id ?? "");
const itemId = String(params.itemId ?? params.item_id ?? "");
if (threadId && itemId) {
handlers.onReasoningSummaryBoundary?.(workspace_id, threadId, itemId);
currentHandlers.onReasoningSummaryBoundary?.(workspace_id, threadId, itemId);
}
return;
}
Expand All @@ -399,7 +408,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const itemId = String(params.itemId ?? params.item_id ?? "");
const delta = String(params.delta ?? "");
if (threadId && itemId && delta) {
handlers.onReasoningTextDelta?.(workspace_id, threadId, itemId, delta);
currentHandlers.onReasoningTextDelta?.(workspace_id, threadId, itemId, delta);
}
return;
}
Expand All @@ -409,7 +418,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const itemId = String(params.itemId ?? params.item_id ?? "");
const delta = String(params.delta ?? "");
if (threadId && itemId && delta) {
handlers.onPlanDelta?.(workspace_id, threadId, itemId, delta);
currentHandlers.onPlanDelta?.(workspace_id, threadId, itemId, delta);
}
return;
}
Expand All @@ -419,7 +428,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const itemId = String(params.itemId ?? params.item_id ?? "");
const delta = String(params.delta ?? "");
if (threadId && itemId && delta) {
handlers.onCommandOutputDelta?.(workspace_id, threadId, itemId, delta);
currentHandlers.onCommandOutputDelta?.(workspace_id, threadId, itemId, delta);
}
return;
}
Expand All @@ -429,7 +438,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const itemId = String(params.itemId ?? params.item_id ?? "");
const stdin = String(params.stdin ?? "");
if (threadId && itemId) {
handlers.onTerminalInteraction?.(workspace_id, threadId, itemId, stdin);
currentHandlers.onTerminalInteraction?.(workspace_id, threadId, itemId, stdin);
}
return;
}
Expand All @@ -439,7 +448,7 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
const itemId = String(params.itemId ?? params.item_id ?? "");
const delta = String(params.delta ?? "");
if (threadId && itemId && delta) {
handlers.onFileChangeOutputDelta?.(workspace_id, threadId, itemId, delta);
currentHandlers.onFileChangeOutputDelta?.(workspace_id, threadId, itemId, delta);
}
return;
}
Expand All @@ -448,5 +457,5 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
return () => {
unlisten();
};
}, [handlers]);
}, []);
}
31 changes: 23 additions & 8 deletions src/features/app/hooks/useRemoteThreadRefreshOnFocus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import type { WorkspaceInfo } from "../../../types";

type UseRemoteThreadRefreshOnFocusOptions = {
Expand All @@ -14,16 +14,28 @@ export function useRemoteThreadRefreshOnFocus({
activeThreadId,
refreshThread,
}: UseRemoteThreadRefreshOnFocusOptions) {
const optionsRef = useRef({ backendMode, activeWorkspace, activeThreadId, refreshThread });
useEffect(() => {
if (backendMode !== "remote") {
return;
}
optionsRef.current = { backendMode, activeWorkspace, activeThreadId, refreshThread };
});

useEffect(() => {
let debounceTimer: ReturnType<typeof setTimeout> | null = null;

const refreshActiveThread = () => {
if (!activeWorkspace?.connected || !activeThreadId) {
return;
if (debounceTimer) {
clearTimeout(debounceTimer);
}
void refreshThread(activeWorkspace.id, activeThreadId);
debounceTimer = setTimeout(() => {
const { backendMode: mode, activeWorkspace: ws, activeThreadId: threadId, refreshThread: refresh } = optionsRef.current;
if (mode !== "remote") {
return;
}
if (!ws?.connected || !threadId) {
return;
}
void refresh(ws.id, threadId);
}, 500);
};

const handleVisibilityChange = () => {
Expand All @@ -37,6 +49,9 @@ export function useRemoteThreadRefreshOnFocus({
return () => {
window.removeEventListener("focus", refreshActiveThread);
document.removeEventListener("visibilitychange", handleVisibilityChange);
if (debounceTimer) {
clearTimeout(debounceTimer);
}
};
}, [activeThreadId, activeWorkspace, backendMode, refreshThread]);
}, []);
}
43 changes: 28 additions & 15 deletions src/features/debug/hooks/useDebugLog.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import { useCallback, useState } from "react";
import { useCallback, useRef, useState } from "react";
import type { DebugEntry } from "../../../types";

const MAX_DEBUG_ENTRIES = 200;

function summarizePayload(payload: unknown): unknown {
if (Array.isArray(payload)) {
return { _type: "array", count: payload.length, sample: payload.slice(0, 5) };
}
if (payload && typeof payload === "object") {
const obj = payload as Record<string, unknown>;
const summarized: Record<string, unknown> = {};
for (const key of Object.keys(obj)) {
if (Array.isArray(obj[key])) {
summarized[key] = { _type: "array", count: (obj[key] as unknown[]).length };
} else {
summarized[key] = obj[key];
}
}
return summarized;
}
return payload;
}

export function useDebugLog() {
const [debugOpen, setDebugOpenState] = useState(false);
const [debugEntries, setDebugEntries] = useState<DebugEntry[]>([]);
const [hasDebugAlerts, setHasDebugAlerts] = useState(false);
const [debugPinned, setDebugPinned] = useState(false);
const debugOpenRef = useRef(debugOpen);
debugOpenRef.current = debugOpen;

const isAlertEntry = useCallback((entry: DebugEntry) => {
if (entry.source === "error" || entry.source === "stderr") {
Expand All @@ -24,27 +45,19 @@ export function useDebugLog() {
return false;
}, []);

const shouldStoreEntry = useCallback(
(entry: DebugEntry) => {
if (debugOpen) {
return true;
}
return isAlertEntry(entry);
},
[debugOpen, isAlertEntry],
);

const addDebugEntry = useCallback(
(entry: DebugEntry) => {
if (!shouldStoreEntry(entry)) {
const isAlert = isAlertEntry(entry);
if (!debugOpenRef.current && !isAlert) {
return;
}
if (isAlertEntry(entry)) {
if (isAlert) {
setHasDebugAlerts(true);
}
setDebugEntries((prev) => [...prev, entry].slice(-MAX_DEBUG_ENTRIES));
const compactEntry = { ...entry, payload: summarizePayload(entry.payload) };
setDebugEntries((prev) => [...prev, compactEntry].slice(-MAX_DEBUG_ENTRIES));
},
[isAlertEntry, shouldStoreEntry],
[isAlertEntry],
);

const handleCopyDebug = useCallback(async () => {
Expand Down
Loading