Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 16 additions & 2 deletions apps/web/src/components/ChatView.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,8 @@ function createSnapshotWithLongProposedPlan(): OrchestrationReadModel {
};
}

function resolveWsRpc(tag: string): unknown {
function resolveWsRpc(body: WsRequestEnvelope["body"]): unknown {
const tag = body._tag;
if (tag === ORCHESTRATION_WS_METHODS.getSnapshot) {
return fixture.snapshot;
}
Expand Down Expand Up @@ -395,6 +396,19 @@ function resolveWsRpc(tag: string): unknown {
truncated: false,
};
}
if (tag === WS_METHODS.terminalOpen) {
return {
threadId: typeof body.threadId === "string" ? body.threadId : THREAD_ID,
terminalId: typeof body.terminalId === "string" ? body.terminalId : "default",
cwd: typeof body.cwd === "string" ? body.cwd : "/repo/project",
status: "running",
pid: 123,
history: "",
exitCode: null,
exitSignal: null,
updatedAt: NOW_ISO,
};
}
return {};
}

Expand Down Expand Up @@ -423,7 +437,7 @@ const worker = setupWorker(
client.send(
JSON.stringify({
id: request.id,
result: resolveWsRpc(method),
result: resolveWsRpc(request.body),
}),
);
});
Expand Down
8 changes: 1 addition & 7 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import { SidebarTrigger } from "./ui/sidebar";
import { newCommandId, newMessageId, newThreadId } from "~/lib/utils";
import { readNativeApi } from "~/nativeApi";
import { resolveAppModelSelection, useAppSettings } from "../appSettings";
import { isTerminalFocused } from "../lib/terminalFocus";
import {
type ComposerImageAttachment,
type DraftThreadEnvMode,
Expand Down Expand Up @@ -1914,13 +1915,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
}, [activeThreadId, focusComposer, terminalState.terminalOpen]);

useEffect(() => {
const isTerminalFocused = (): boolean => {
const activeElement = document.activeElement;
if (!(activeElement instanceof HTMLElement)) return false;
if (activeElement.classList.contains("xterm-helper-textarea")) return true;
return activeElement.closest(".thread-terminal-drawer .xterm") !== null;
};

const handler = (event: globalThis.KeyboardEvent) => {
if (!activeThreadId || event.defaultPrevented) return;
const shortcutContext = {
Expand Down
130 changes: 6 additions & 124 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-
import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { CSS } from "@dnd-kit/utilities";
import {
DEFAULT_RUNTIME_MODE,
DEFAULT_MODEL_BY_PROVIDER,
type DesktopUpdateState,
ProjectId,
Expand All @@ -40,14 +39,15 @@ import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
import { useAppSettings } from "../appSettings";
import { isElectron } from "../env";
import { APP_STAGE_LABEL, APP_VERSION } from "../branding";
import { isMacPlatform, newCommandId, newProjectId, newThreadId } from "../lib/utils";
import { isMacPlatform, newCommandId, newProjectId } from "../lib/utils";
import { useStore } from "../store";
import { isChatNewLocalShortcut, isChatNewShortcut, shortcutLabelForCommand } from "../keybindings";
import { shortcutLabelForCommand } from "../keybindings";
import { derivePendingApprovals, derivePendingUserInputs } from "../session-logic";
import { gitRemoveWorktreeMutationOptions, gitStatusQueryOptions } from "../lib/gitReactQuery";
import { serverConfigQueryOptions } from "../lib/serverReactQuery";
import { readNativeApi } from "../nativeApi";
import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraftStore";
import { useComposerDraftStore } from "../composerDraftStore";
import { useHandleNewThread } from "../hooks/useHandleNewThread";
import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
import { toastManager } from "./ui/toast";
import {
Expand Down Expand Up @@ -262,11 +262,8 @@ export default function Sidebar() {
const getDraftThreadByProjectId = useComposerDraftStore(
(store) => store.getDraftThreadByProjectId,
);
const getDraftThread = useComposerDraftStore((store) => store.getDraftThread);
const terminalStateByThreadId = useTerminalStateStore((state) => state.terminalStateByThreadId);
const clearTerminalState = useTerminalStateStore((state) => state.clearTerminalState);
const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId);
const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
const clearProjectDraftThreadId = useComposerDraftStore(
(store) => store.clearProjectDraftThreadId,
);
Expand All @@ -276,6 +273,7 @@ export default function Sidebar() {
const navigate = useNavigate();
const isOnSettings = useLocation({ select: (loc) => loc.pathname === "/settings" });
const { settings: appSettings } = useAppSettings();
const { handleNewThread } = useHandleNewThread();
const routeThreadId = useParams({
strict: false,
select: (params) => (params.threadId ? ThreadId.makeUnsafe(params.threadId) : null),
Expand Down Expand Up @@ -384,80 +382,6 @@ export default function Sidebar() {
});
}, []);

const handleNewThread = useCallback(
(
projectId: ProjectId,
options?: {
branch?: string | null;
worktreePath?: string | null;
envMode?: DraftThreadEnvMode;
},
): Promise<void> => {
const hasBranchOption = options?.branch !== undefined;
const hasWorktreePathOption = options?.worktreePath !== undefined;
const hasEnvModeOption = options?.envMode !== undefined;
const storedDraftThread = getDraftThreadByProjectId(projectId);
if (storedDraftThread) {
return (async () => {
if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) {
setDraftThreadContext(storedDraftThread.threadId, {
...(hasBranchOption ? { branch: options?.branch ?? null } : {}),
...(hasWorktreePathOption ? { worktreePath: options?.worktreePath ?? null } : {}),
...(hasEnvModeOption ? { envMode: options?.envMode } : {}),
});
}
setProjectDraftThreadId(projectId, storedDraftThread.threadId);
if (routeThreadId === storedDraftThread.threadId) {
return;
}
await navigate({
to: "/$threadId",
params: { threadId: storedDraftThread.threadId },
});
})();
}
clearProjectDraftThreadId(projectId);

const activeDraftThread = routeThreadId ? getDraftThread(routeThreadId) : null;
if (activeDraftThread && routeThreadId && activeDraftThread.projectId === projectId) {
if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) {
setDraftThreadContext(routeThreadId, {
...(hasBranchOption ? { branch: options?.branch ?? null } : {}),
...(hasWorktreePathOption ? { worktreePath: options?.worktreePath ?? null } : {}),
...(hasEnvModeOption ? { envMode: options?.envMode } : {}),
});
}
setProjectDraftThreadId(projectId, routeThreadId);
return Promise.resolve();
}
const threadId = newThreadId();
const createdAt = new Date().toISOString();
return (async () => {
setProjectDraftThreadId(projectId, threadId, {
createdAt,
branch: options?.branch ?? null,
worktreePath: options?.worktreePath ?? null,
envMode: options?.envMode ?? "local",
runtimeMode: DEFAULT_RUNTIME_MODE,
});

await navigate({
to: "/$threadId",
params: { threadId },
});
})();
},
[
clearProjectDraftThreadId,
getDraftThreadByProjectId,
navigate,
getDraftThread,
routeThreadId,
setDraftThreadContext,
setProjectDraftThreadId,
],
);

const focusMostRecentThreadForProject = useCallback(
(projectId: ProjectId) => {
const latestThread = threads
Expand Down Expand Up @@ -1023,60 +947,18 @@ export default function Sidebar() {
);

useEffect(() => {
const onWindowKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && selectedThreadIds.size > 0) {
event.preventDefault();
clearSelection();
return;
}

const activeThread = routeThreadId
? threads.find((thread) => thread.id === routeThreadId)
: undefined;
const activeDraftThread = routeThreadId ? getDraftThread(routeThreadId) : null;
if (isChatNewLocalShortcut(event, keybindings)) {
const projectId =
activeThread?.projectId ?? activeDraftThread?.projectId ?? projects[0]?.id;
if (!projectId) return;
event.preventDefault();
void handleNewThread(projectId);
return;
}

if (!isChatNewShortcut(event, keybindings)) return;
const projectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? projects[0]?.id;
if (!projectId) return;
event.preventDefault();
void handleNewThread(projectId, {
branch: activeThread?.branch ?? activeDraftThread?.branch ?? null,
worktreePath: activeThread?.worktreePath ?? activeDraftThread?.worktreePath ?? null,
envMode: activeDraftThread?.envMode ?? (activeThread?.worktreePath ? "worktree" : "local"),
});
};

const onMouseDown = (event: globalThis.MouseEvent) => {
if (selectedThreadIds.size === 0) return;
const target = event.target instanceof HTMLElement ? event.target : null;
if (!shouldClearThreadSelectionOnMouseDown(target)) return;
clearSelection();
};

window.addEventListener("keydown", onWindowKeyDown);
window.addEventListener("mousedown", onMouseDown);
return () => {
window.removeEventListener("keydown", onWindowKeyDown);
window.removeEventListener("mousedown", onMouseDown);
};
}, [
clearSelection,
getDraftThread,
handleNewThread,
keybindings,
projects,
routeThreadId,
selectedThreadIds.size,
threads,
]);
}, [clearSelection, selectedThreadIds.size]);

useEffect(() => {
if (!isElectron) return;
Expand Down
113 changes: 113 additions & 0 deletions apps/web/src/hooks/useHandleNewThread.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { DEFAULT_RUNTIME_MODE, type ProjectId, ThreadId } from "@t3tools/contracts";
import { useNavigate, useParams } from "@tanstack/react-router";
import { useCallback } from "react";
import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraftStore";
import { newThreadId } from "../lib/utils";
import { useStore } from "../store";

export function useHandleNewThread() {
const projects = useStore((store) => store.projects);
const threads = useStore((store) => store.threads);
const getDraftThreadByProjectId = useComposerDraftStore(
(store) => store.getDraftThreadByProjectId,
);
const getDraftThread = useComposerDraftStore((store) => store.getDraftThread);
const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId);
const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
const clearProjectDraftThreadId = useComposerDraftStore(
(store) => store.clearProjectDraftThreadId,
);
const navigate = useNavigate();
const routeThreadId = useParams({
strict: false,
select: (params) => (params.threadId ? ThreadId.makeUnsafe(params.threadId) : null),
});

const activeThread = routeThreadId
? threads.find((thread) => thread.id === routeThreadId)
: undefined;
const activeDraftThread = routeThreadId ? getDraftThread(routeThreadId) : null;
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

const handleNewThread = useCallback(
(
projectId: ProjectId,
options?: {
branch?: string | null;
worktreePath?: string | null;
envMode?: DraftThreadEnvMode;
},
): Promise<void> => {
const hasBranchOption = options?.branch !== undefined;
const hasWorktreePathOption = options?.worktreePath !== undefined;
const hasEnvModeOption = options?.envMode !== undefined;
const storedDraftThread = getDraftThreadByProjectId(projectId);
if (storedDraftThread) {
return (async () => {
if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) {
setDraftThreadContext(storedDraftThread.threadId, {
...(hasBranchOption ? { branch: options?.branch ?? null } : {}),
...(hasWorktreePathOption ? { worktreePath: options?.worktreePath ?? null } : {}),
...(hasEnvModeOption ? { envMode: options?.envMode } : {}),
});
}
setProjectDraftThreadId(projectId, storedDraftThread.threadId);
if (routeThreadId === storedDraftThread.threadId) {
return;
}
await navigate({
to: "/$threadId",
params: { threadId: storedDraftThread.threadId },
});
})();
}

clearProjectDraftThreadId(projectId);

if (activeDraftThread && routeThreadId && activeDraftThread.projectId === projectId) {
if (hasBranchOption || hasWorktreePathOption || hasEnvModeOption) {
setDraftThreadContext(routeThreadId, {
...(hasBranchOption ? { branch: options?.branch ?? null } : {}),
...(hasWorktreePathOption ? { worktreePath: options?.worktreePath ?? null } : {}),
...(hasEnvModeOption ? { envMode: options?.envMode } : {}),
});
}
setProjectDraftThreadId(projectId, routeThreadId);
return Promise.resolve();
}

const threadId = newThreadId();
const createdAt = new Date().toISOString();
return (async () => {
setProjectDraftThreadId(projectId, threadId, {
createdAt,
branch: options?.branch ?? null,
worktreePath: options?.worktreePath ?? null,
envMode: options?.envMode ?? "local",
runtimeMode: DEFAULT_RUNTIME_MODE,
});

await navigate({
to: "/$threadId",
params: { threadId },
});
})();
},
[
activeDraftThread,
clearProjectDraftThreadId,
getDraftThreadByProjectId,
navigate,
routeThreadId,
setDraftThreadContext,
setProjectDraftThreadId,
],
);

return {
activeDraftThread,
activeThread,
handleNewThread,
projects,
routeThreadId,
};
}
6 changes: 6 additions & 0 deletions apps/web/src/lib/terminalFocus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function isTerminalFocused(): boolean {
const activeElement = document.activeElement;
if (!(activeElement instanceof HTMLElement)) return false;
if (activeElement.classList.contains("xterm-helper-textarea")) return true;
return activeElement.closest(".thread-terminal-drawer .xterm") !== null;
}
Loading
Loading