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
82 changes: 56 additions & 26 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,16 @@ import {
normalizeModelSlug,
resolveModelSlugForProvider,
} from "@okcode/shared/model";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import {
Suspense,
lazy,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useDebouncedValue } from "@tanstack/react-pacer";
import { useNavigate } from "@tanstack/react-router";
Expand Down Expand Up @@ -100,7 +109,6 @@ import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"
import { buildChatShortcutGuides } from "~/lib/chatShortcutGuidance";
import { dispatchGitPullRequestAction } from "~/lib/gitPullRequestAction";
import PlanSidebar from "./PlanSidebar";
import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
import {
AtSignIcon,
BotIcon,
Expand Down Expand Up @@ -229,6 +237,23 @@ import { useTransportState } from "~/hooks/useTransportState";
import { hasCustomThreadTitle, normalizeThreadTitle } from "~/threadTitle";
import { enhancePrompt, type PromptEnhancementId } from "../promptEnhancement";

function preloadThreadTerminalDrawer() {
return import("./ThreadTerminalDrawer");
}

const ThreadTerminalDrawer = lazy(preloadThreadTerminalDrawer);

function TerminalDrawerLoadingFallback(props: { height: number }) {
return (
<div
className="flex items-center justify-center border-t border-border/60 bg-background/95 text-muted-foreground/60"
style={{ height: `${props.height}px` }}
>
<span className="text-xs">Loading terminal...</span>
</div>
);
}

const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000;
const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;
const FILE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_FILE_BYTES / (1024 * 1024))}MB`;
Expand Down Expand Up @@ -4846,6 +4871,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
onDeleteProjectScript={deleteProjectScript}
onImportProjectScripts={importProjectScripts}
onToggleTerminal={toggleTerminalVisibility}
onPrefetchTerminal={preloadThreadTerminalDrawer}
onToggleCodeViewer={toggleCodeViewer}
onToggleDiffViewer={handleToggleDiffViewer}
onTogglePreview={() => activeProjectId && togglePreviewOpen(activeProjectId)}
Expand Down Expand Up @@ -5761,30 +5787,34 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
return null;
}
return (
<ThreadTerminalDrawer
key={activeThread.id}
threadId={activeThread.id}
cwd={gitCwd ?? activeProject.cwd}
runtimeEnv={threadTerminalRuntimeEnv}
height={terminalState.terminalHeight}
terminalIds={terminalState.terminalIds}
activeTerminalId={terminalState.activeTerminalId}
terminalGroups={terminalState.terminalGroups}
activeTerminalGroupId={terminalState.activeTerminalGroupId}
focusRequestId={terminalFocusRequestId}
onSplitTerminal={splitTerminal}
onNewTerminal={createNewTerminal}
splitShortcutLabel={splitTerminalShortcutLabel ?? undefined}
newShortcutLabel={newTerminalShortcutLabel ?? undefined}
closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
onActiveTerminalChange={activateTerminal}
onCloseTerminal={closeTerminal}
onCollapseTerminal={toggleTerminalVisibility}
onHeightChange={setTerminalHeight}
onAddTerminalContext={addTerminalContextToDraft}
onSendTerminalContext={sendSelectedTerminalContext}
onPreviewUrl={onPreviewUrl}
/>
<Suspense
fallback={<TerminalDrawerLoadingFallback height={terminalState.terminalHeight} />}
>
<ThreadTerminalDrawer
key={activeThread.id}
threadId={activeThread.id}
cwd={gitCwd ?? activeProject.cwd}
runtimeEnv={threadTerminalRuntimeEnv}
height={terminalState.terminalHeight}
terminalIds={terminalState.terminalIds}
activeTerminalId={terminalState.activeTerminalId}
terminalGroups={terminalState.terminalGroups}
activeTerminalGroupId={terminalState.activeTerminalGroupId}
focusRequestId={terminalFocusRequestId}
onSplitTerminal={splitTerminal}
onNewTerminal={createNewTerminal}
splitShortcutLabel={splitTerminalShortcutLabel ?? undefined}
newShortcutLabel={newTerminalShortcutLabel ?? undefined}
closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
onActiveTerminalChange={activateTerminal}
onCloseTerminal={closeTerminal}
onCollapseTerminal={toggleTerminalVisibility}
onHeightChange={setTerminalHeight}
onAddTerminalContext={addTerminalContextToDraft}
onSendTerminalContext={sendSelectedTerminalContext}
onPreviewUrl={onPreviewUrl}
/>
</Suspense>
);
})()}

Expand Down
38 changes: 30 additions & 8 deletions apps/web/src/components/ThreadTerminalDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
} from "react";
import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
import { type TerminalContextSelection } from "~/lib/terminalContext";
import { openInPreferredEditor } from "../editorPreferences";
import { openFileReference } from "../fileOpen";
import { useCodeViewerStore } from "../codeViewerStore";
import {
extractTerminalLinks,
isTerminalLinkActivation,
Expand Down Expand Up @@ -244,11 +245,13 @@ function TerminalViewport({
const onPreviewUrlRef = useRef(onPreviewUrl);
const terminalLabelRef = useRef(terminalLabel);
const hasHandledExitRef = useRef(false);
const openFileInViewer = useCodeViewerStore((state) => state.openFile);
const selectionPointerRef = useRef<{ x: number; y: number } | null>(null);
const selectionGestureActiveRef = useRef(false);
const selectionActionRequestIdRef = useRef(0);
const selectionActionOpenRef = useRef(false);
const selectionActionTimerRef = useRef<number | null>(null);
const historyReplayFrameRef = useRef<number | null>(null);
const [hoverLine, setHoverLine] = useState<{
bufferLine: number;
top: number;
Expand Down Expand Up @@ -467,7 +470,13 @@ function TerminalViewport({
}

const target = resolvePathLinkTarget(match.text, cwd);
void openInPreferredEditor(api, target).catch((error) => {
void openFileReference({
api,
cwd,
targetPath: target,
preferExternal: false,
openInViewer: openFileInViewer,
}).catch((error) => {
writeSystemMessage(
latestTerminal,
error instanceof Error ? error.message : "Unable to open path",
Expand Down Expand Up @@ -537,6 +546,20 @@ function TerminalViewport({
attributeFilter: ["class", "style"],
});

const scheduleHistoryReplay = (history: string) => {
if (history.length === 0 || historyReplayFrameRef.current !== null) {
return;
}

historyReplayFrameRef.current = window.requestAnimationFrame(() => {
historyReplayFrameRef.current = null;
if (disposed) return;
const activeTerminal = terminalRef.current;
if (!activeTerminal) return;
activeTerminal.write(history);
});
};

const openTerminal = async () => {
try {
const activeTerminal = terminalRef.current;
Expand All @@ -553,9 +576,7 @@ function TerminalViewport({
});
if (disposed) return;
activeTerminal.write("\u001bc");
if (snapshot.history.length > 0) {
activeTerminal.write(snapshot.history);
}
scheduleHistoryReplay(snapshot.history);
if (autoFocus) {
window.requestAnimationFrame(() => {
activeTerminal.focus();
Expand Down Expand Up @@ -585,9 +606,7 @@ function TerminalViewport({
hasHandledExitRef.current = false;
clearSelectionAction();
activeTerminal.write("\u001bc");
if (event.snapshot.history.length > 0) {
activeTerminal.write(event.snapshot.history);
}
scheduleHistoryReplay(event.snapshot.history);
return;
}

Expand Down Expand Up @@ -658,6 +677,9 @@ function TerminalViewport({
if (selectionActionTimerRef.current !== null) {
window.clearTimeout(selectionActionTimerRef.current);
}
if (historyReplayFrameRef.current !== null) {
window.cancelAnimationFrame(historyReplayFrameRef.current);
}
window.removeEventListener("mouseup", handleMouseUp);
mount.removeEventListener("pointerdown", handlePointerDown);
themeObserver.disconnect();
Expand Down
23 changes: 15 additions & 8 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ interface ChatHeaderProps {
onDeleteProjectScript: (scriptId: string) => Promise<void>;
onImportProjectScripts: (scripts: ProjectScriptDraft[]) => Promise<void>;
onToggleTerminal: () => void;
onToggleCodeViewer?: () => void;
onToggleDiffViewer?: () => void;
onTogglePreview?: () => void;
onTogglePreviewLayout?: () => void;
onPrefetchTerminal: () => void;
onToggleCodeViewer: () => void;
onToggleDiffViewer: () => void;
onTogglePreview: () => void;
onTogglePreviewLayout: () => void;
onMinimize?: (() => void) | undefined;
}

Expand All @@ -77,8 +78,8 @@ export const ChatHeader = memo(function ChatHeader({
terminalAvailable,
terminalOpen,
terminalToggleShortcutLabel,
codeViewerOpen: _codeViewerOpen,
diffViewerOpen: _diffViewerOpen,
codeViewerOpen,
diffViewerOpen,
previewAvailable,
previewOpen,
previewDock: _previewDock,
Expand All @@ -91,8 +92,9 @@ export const ChatHeader = memo(function ChatHeader({
onDeleteProjectScript,
onImportProjectScripts,
onToggleTerminal,
onToggleCodeViewer: _onToggleCodeViewer,
onToggleDiffViewer: _onToggleDiffViewer,
onPrefetchTerminal,
onToggleCodeViewer,
onToggleDiffViewer,
onTogglePreview,
onTogglePreviewLayout: _onTogglePreviewLayout,
onMinimize,
Expand Down Expand Up @@ -255,6 +257,11 @@ export const ChatHeader = memo(function ChatHeader({
terminalAvailable={terminalAvailable}
terminalOpen={terminalOpen}
terminalToggleShortcutLabel={terminalToggleShortcutLabel}
onPrefetchTerminal={onPrefetchTerminal}
codeViewerOpen={codeViewerOpen}
onToggleCodeViewer={onToggleCodeViewer}
diffViewerOpen={diffViewerOpen}
onToggleDiffViewer={onToggleDiffViewer}
previewAvailable={previewAvailable}
previewOpen={previewOpen}
onToggleTerminal={onToggleTerminal}
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/components/chat/HeaderPanelsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ interface HeaderPanelsMenuProps {
terminalAvailable: boolean;
terminalOpen: boolean;
terminalToggleShortcutLabel: string | null;
onPrefetchTerminal: () => void;
codeViewerOpen: boolean;
onToggleCodeViewer: () => void;
diffViewerOpen: boolean;
onToggleDiffViewer: () => void;
previewAvailable: boolean;
previewOpen: boolean;
onToggleTerminal: () => void;
Expand All @@ -24,6 +29,11 @@ export const HeaderPanelsMenu = memo(function HeaderPanelsMenu({
terminalAvailable,
terminalOpen,
terminalToggleShortcutLabel,
onPrefetchTerminal,
codeViewerOpen,
onToggleCodeViewer,
diffViewerOpen,
onToggleDiffViewer,
previewAvailable,
previewOpen,
onToggleTerminal,
Expand Down Expand Up @@ -66,6 +76,8 @@ export const HeaderPanelsMenu = memo(function HeaderPanelsMenu({
value="terminal"
disabled={!terminalAvailable}
onClick={onToggleTerminal}
onPointerEnter={onPrefetchTerminal}
onFocus={onPrefetchTerminal}
aria-label="Toggle terminal"
>
<TerminalSquareIcon className="size-3.5" />
Expand Down
24 changes: 22 additions & 2 deletions apps/web/src/fileOpen.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { resolveCodeViewerRelativePath, splitFileTargetPosition } from "./fileOpen";
import { describe, expect, it, vi } from "vitest";
import {
openFileReference,
resolveCodeViewerRelativePath,
splitFileTargetPosition,
} from "./fileOpen";

describe("splitFileTargetPosition", () => {
it("extracts line and column suffixes", () => {
Expand Down Expand Up @@ -35,3 +39,19 @@ describe("resolveCodeViewerRelativePath", () => {
).toBeNull();
});
});

describe("openFileReference", () => {
it("opens files in the code viewer when external editors are not preferred", async () => {
const openInViewer = vi.fn();

await openFileReference({
api: {} as never,
cwd: "/Users/julius/project",
targetPath: "/Users/julius/project/src/main.ts:12:4",
preferExternal: false,
openInViewer,
});

expect(openInViewer).toHaveBeenCalledWith("/Users/julius/project", "src/main.ts");
});
});
Loading