🐛 FIX: new session slow
This commit is contained in:
parent
4d2fb1e2a0
commit
312fb33859
@ -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 }));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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={{
|
||||
|
||||
@ -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: () => {
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user