🐛 FIX: new session slow

This commit is contained in:
kumarabhirup 2026-02-15 23:54:05 -08:00
parent 4d2fb1e2a0
commit 312fb33859
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
5 changed files with 63 additions and 87 deletions

View File

@ -1,28 +0,0 @@
import { runAgent } from "@/lib/agent-runner";
// Force Node.js runtime (required for child_process)
export const runtime = "nodejs";
export const maxDuration = 30;
/** POST /api/new-session — send /new to the agent to start a fresh backend session */
export async function POST() {
return new Promise<Response>((resolve) => {
runAgent("/new", undefined, {
onTextDelta: () => {},
onThinkingDelta: () => {},
onToolStart: () => {},
onToolEnd: () => {},
onLifecycleEnd: () => {},
onError: (err) => {
console.error("[new-session] Error:", err);
resolve(
Response.json({ ok: false, error: err.message }, { status: 500 }),
);
},
onClose: () => {
resolve(Response.json({ ok: true }));
},
});
});
}

View File

@ -485,7 +485,6 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
string | null
>(null);
const [loadingSession, setLoadingSession] = useState(false);
const [startingNewSession, setStartingNewSession] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
// ── Attachment state ──
@ -1116,7 +1115,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
],
);
const handleNewSession = useCallback(async () => {
const handleNewSession = useCallback(() => {
reconnectAbortRef.current?.abort();
void stop();
setIsReconnecting(false);
@ -1128,20 +1127,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
isFirstFileMessageRef.current = true;
newSessionPendingRef.current = false;
setQueuedMessages([]);
if (!filePath) {
setStartingNewSession(true);
try {
await fetch("/api/new-session", {
method: "POST",
});
} catch (err) {
console.error("Failed to send /new:", err);
} finally {
setStartingNewSession(false);
}
}
}, [setMessages, onActiveSessionChange, filePath, stop]);
}, [setMessages, onActiveSessionChange, stop]);
// Keep the ref in sync so handleEditorSubmit can call it
handleNewSessionRef.current = handleNewSession;
@ -1247,11 +1233,9 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
// ── Status label ──
const statusLabel = startingNewSession
? "Starting new session..."
: loadingSession
? "Loading session..."
: isReconnecting
const statusLabel = loadingSession
? "Loading session..."
: isReconnecting
? "Resuming stream..."
: status === "ready"
? "Ready"
@ -1625,10 +1609,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
? "Add a message or send files..."
: "Type @ to mention files..."
}
disabled={
loadingSession ||
startingNewSession
}
disabled={loadingSession}
compact={compact}
/>
@ -1710,8 +1691,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
(editorEmpty &&
attachedFiles.length ===
0) ||
loadingSession ||
startingNewSession
loadingSession
}
className={`${compact ? "w-6 h-6" : "w-7 h-7"} rounded-full flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed`}
style={{

View File

@ -531,7 +531,16 @@ export function createFileMentionRenderer() {
root = null;
return true;
}
return componentRef.current?.onKeyDown(props) ?? false;
const handled = componentRef.current?.onKeyDown(props) ?? false;
if (handled) {
// Stop the chat-editor's DOM keydown listener from
// also firing and submitting the message. By the time
// that listener runs, the suggestion command has already
// executed and the plugin state is inactive, so the
// `suggestState.active` guard would not catch it.
props.event.stopImmediatePropagation();
}
return handled;
},
onExit: () => {

View File

@ -710,12 +710,14 @@ export function FileManagerTree({ tree, activePath, onSelect, onRefresh, compact
const [activeNode, setActiveNode] = useState<TreeNode | null>(null);
// Track pointer position during @dnd-kit drags for cross-component drops.
// Capture-phase listener on window works even when @dnd-kit has pointer capture.
// Installed synchronously in handleDragStart (not useEffect) to avoid
// missing early pointer moves. Capture-phase on window fires before
// @dnd-kit's own document-level listener.
const pointerPosRef = useRef({ x: 0, y: 0 });
useEffect(() => {
if (!activeNode) {return;}
const pointerListenerRef = useRef<((e: PointerEvent) => void) | null>(null);
const onPointerMove = (e: PointerEvent) => {
const installPointerTracker = useCallback(() => {
const handler = (e: PointerEvent) => {
pointerPosRef.current = { x: e.clientX, y: e.clientY };
// Toggle visual drop indicator on external chat drop target
@ -729,14 +731,20 @@ export function FileManagerTree({ tree, activePath, onSelect, onRefresh, compact
prev.removeAttribute("data-drag-hover");
}
};
pointerListenerRef.current = handler;
window.addEventListener("pointermove", handler, true);
}, []);
window.addEventListener("pointermove", onPointerMove, true);
return () => {
window.removeEventListener("pointermove", onPointerMove, true);
// Clean up any lingering highlight
document.querySelector("[data-drag-hover]")?.removeAttribute("data-drag-hover");
};
}, [activeNode]);
const removePointerTracker = useCallback(() => {
if (pointerListenerRef.current) {
window.removeEventListener("pointermove", pointerListenerRef.current, true);
pointerListenerRef.current = null;
}
document.querySelector("[data-drag-hover]")?.removeAttribute("data-drag-hover");
}, []);
// Clean up on unmount
useEffect(() => removePointerTracker, [removePointerTracker]);
// Context menu state
const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; target: ContextMenuTarget } | null>(null);
@ -781,8 +789,11 @@ export function FileManagerTree({ tree, activePath, onSelect, onRefresh, compact
const handleDragStart = useCallback((event: DragStartEvent) => {
const data = event.active.data.current as { node: TreeNode } | undefined;
if (data?.node) {setActiveNode(data.node);}
}, []);
if (data?.node) {
setActiveNode(data.node);
installPointerTracker();
}
}, [installPointerTracker]);
const handleDragOver = useCallback((event: DragOverEvent) => {
const overData = event.over?.data.current as { node?: TreeNode; rootDrop?: boolean } | undefined;
@ -811,14 +822,28 @@ export function FileManagerTree({ tree, activePath, onSelect, onRefresh, compact
async (event: DragEndEvent) => {
setActiveNode(null);
setDragOverPath(null);
removePointerTracker();
const activeData = event.active.data.current as { node: TreeNode } | undefined;
const overData = event.over?.data.current as { node?: TreeNode; rootDrop?: boolean } | undefined;
if (!activeData?.node) {return;}
const source = activeData.node;
// Check for external drop targets FIRST (e.g. chat input).
// closestCenter always returns a droppable even when the pointer is
// far outside the tree, so we can't rely on `event.over === null`.
if (onExternalDrop) {
const { x, y } = pointerPosRef.current;
const el = document.elementFromPoint(x, y);
if (el?.closest("[data-chat-drop-target]")) {
onExternalDrop(source);
return;
}
}
const overData = event.over?.data.current as { node?: TreeNode; rootDrop?: boolean } | undefined;
// Drop onto root level
if (overData?.rootDrop) {
// Already at root? No-op
@ -830,17 +855,7 @@ export function FileManagerTree({ tree, activePath, onSelect, onRefresh, compact
return;
}
// No @dnd-kit droppable: check for external drop targets (e.g. chat input)
if (!overData?.node) {
if (onExternalDrop) {
const { x, y } = pointerPosRef.current;
const el = document.elementFromPoint(x, y);
if (el?.closest("[data-chat-drop-target]")) {
onExternalDrop(source);
}
}
return;
}
if (!overData?.node) {return;}
const target = overData.node;
@ -858,14 +873,14 @@ export function FileManagerTree({ tree, activePath, onSelect, onRefresh, compact
onRefresh();
}
},
[onRefresh, onExternalDrop],
[onRefresh, onExternalDrop, removePointerTracker],
);
const handleDragCancel = useCallback(() => {
setActiveNode(null);
setDragOverPath(null);
document.querySelector("[data-drag-hover]")?.removeAttribute("data-drag-hover");
}, []);
removePointerTracker();
}, [removePointerTracker]);
// Context menu handlers
const handleContextMenu = useCallback((e: React.MouseEvent, node: TreeNode) => {
@ -1212,8 +1227,8 @@ export function FileManagerTree({ tree, activePath, onSelect, onRefresh, compact
<RootDropZone isDragging={!!activeNode} />
</div>
{/* Drag overlay (ghost) */}
<DragOverlay dropAnimation={null}>
{/* Drag overlay (ghost) — pointer-events:none so elementFromPoint sees through it */}
<DragOverlay dropAnimation={null} style={{ pointerEvents: "none" }}>
{activeNode ? <DragOverlayContent node={activeNode} /> : null}
</DragOverlay>

File diff suppressed because one or more lines are too long