Skip to content

Commit 7954880

Browse files
committed
makes terminal more adjustable
1 parent 9793b4f commit 7954880

6 files changed

Lines changed: 164 additions & 26 deletions

File tree

apps/web/src/components/ChatView.browser.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,6 +1911,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
19111911
[THREAD_KEY]: {
19121912
terminalOpen: true,
19131913
terminalHeight: 280,
1914+
terminalWidth: 900,
19141915
terminalIds: ["default"],
19151916
runningTerminalIds: [],
19161917
activeTerminalId: "default",

apps/web/src/components/ChatView.tsx

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import {
8989
DEFAULT_INTERACTION_MODE,
9090
DEFAULT_RUNTIME_MODE,
9191
DEFAULT_THREAD_TERMINAL_ID,
92+
DEFAULT_THREAD_TERMINAL_WIDTH,
9293
MAX_TERMINALS_PER_GROUP,
9394
type ChatMessage,
9495
type SessionPhase,
@@ -429,6 +430,17 @@ function useLocalDispatchState(input: {
429430
};
430431
}
431432

433+
const MIN_FLOATING_TERMINAL_WIDTH = 400;
434+
const MAX_FLOATING_TERMINAL_WIDTH_RATIO = 0.97;
435+
436+
function clampFloatingTerminalWidth(w: number): number {
437+
if (typeof window === "undefined") return DEFAULT_THREAD_TERMINAL_WIDTH;
438+
return Math.min(
439+
Math.max(Math.round(w), MIN_FLOATING_TERMINAL_WIDTH),
440+
Math.floor(window.innerWidth * MAX_FLOATING_TERMINAL_WIDTH_RATIO),
441+
);
442+
}
443+
432444
interface PersistentThreadTerminalDrawerProps {
433445
threadRef: { environmentId: EnvironmentId; threadId: ThreadId };
434446
threadId: ThreadId;
@@ -468,13 +480,27 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
468480
selectThreadTerminalState(state.terminalStateByThreadKey, threadRef),
469481
);
470482
const storeSetTerminalHeight = useTerminalStateStore((state) => state.setTerminalHeight);
483+
const storeSetTerminalWidth = useTerminalStateStore((state) => state.setTerminalWidth);
471484
const storeSplitTerminal = useTerminalStateStore((state) => state.splitTerminal);
472485
const storeNewTerminal = useTerminalStateStore((state) => state.newTerminal);
473486
const storeSetActiveTerminal = useTerminalStateStore((state) => state.setActiveTerminal);
474487
const storeCloseTerminal = useTerminalStateStore((state) => state.closeTerminal);
475488
const storeSetTerminalOpen = useTerminalStateStore((state) => state.setTerminalOpen);
476489
const [localFocusRequestId, setLocalFocusRequestId] = useState(0);
477490
const floatingTerminalTitleId = useId();
491+
492+
const [floatingWidth, setFloatingWidth] = useState(() =>
493+
clampFloatingTerminalWidth(terminalState.terminalWidth),
494+
);
495+
const floatingWidthRef = useRef(floatingWidth);
496+
const widthResizeStateRef = useRef<{
497+
pointerId: number;
498+
side: "left" | "right";
499+
startX: number;
500+
startWidth: number;
501+
} | null>(null);
502+
const didWidthResizeDuringDragRef = useRef(false);
503+
478504
const worktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null;
479505
const effectiveWorktreePath = useMemo(() => {
480506
if (launchContext !== null) {
@@ -518,6 +544,87 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
518544
[storeSetTerminalHeight, threadRef],
519545
);
520546

547+
const setTerminalWidth = useCallback(
548+
(width: number) => {
549+
storeSetTerminalWidth(threadRef, width);
550+
},
551+
[storeSetTerminalWidth, threadRef],
552+
);
553+
554+
useEffect(() => {
555+
floatingWidthRef.current = floatingWidth;
556+
}, [floatingWidth]);
557+
558+
useEffect(() => {
559+
if (widthResizeStateRef.current) return;
560+
const clamped = clampFloatingTerminalWidth(terminalState.terminalWidth);
561+
floatingWidthRef.current = clamped;
562+
setFloatingWidth(clamped);
563+
}, [terminalState.terminalWidth, threadId]);
564+
565+
const handleWidthResizePointerDownLeft = useCallback(
566+
(event: React.PointerEvent<HTMLDivElement>) => {
567+
if (event.button !== 0) return;
568+
event.preventDefault();
569+
event.currentTarget.setPointerCapture(event.pointerId);
570+
didWidthResizeDuringDragRef.current = false;
571+
widthResizeStateRef.current = {
572+
pointerId: event.pointerId,
573+
side: "left",
574+
startX: event.clientX,
575+
startWidth: floatingWidthRef.current,
576+
};
577+
},
578+
[],
579+
);
580+
581+
const handleWidthResizePointerDownRight = useCallback(
582+
(event: React.PointerEvent<HTMLDivElement>) => {
583+
if (event.button !== 0) return;
584+
event.preventDefault();
585+
event.currentTarget.setPointerCapture(event.pointerId);
586+
didWidthResizeDuringDragRef.current = false;
587+
widthResizeStateRef.current = {
588+
pointerId: event.pointerId,
589+
side: "right",
590+
startX: event.clientX,
591+
startWidth: floatingWidthRef.current,
592+
};
593+
},
594+
[],
595+
);
596+
597+
const handleWidthResizePointerMove = useCallback(
598+
(event: React.PointerEvent<HTMLDivElement>) => {
599+
const state = widthResizeStateRef.current;
600+
if (!state || state.pointerId !== event.pointerId) return;
601+
event.preventDefault();
602+
const delta = event.clientX - state.startX;
603+
const rawWidth =
604+
state.side === "right" ? state.startWidth + delta : state.startWidth - delta;
605+
const clamped = clampFloatingTerminalWidth(rawWidth);
606+
if (clamped === floatingWidthRef.current) return;
607+
didWidthResizeDuringDragRef.current = true;
608+
floatingWidthRef.current = clamped;
609+
setFloatingWidth(clamped);
610+
},
611+
[],
612+
);
613+
614+
const handleWidthResizePointerEnd = useCallback(
615+
(event: React.PointerEvent<HTMLDivElement>) => {
616+
const state = widthResizeStateRef.current;
617+
if (!state || state.pointerId !== event.pointerId) return;
618+
widthResizeStateRef.current = null;
619+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
620+
event.currentTarget.releasePointerCapture(event.pointerId);
621+
}
622+
if (!didWidthResizeDuringDragRef.current) return;
623+
setTerminalWidth(floatingWidthRef.current);
624+
},
625+
[setTerminalWidth],
626+
);
627+
521628
const splitTerminal = useCallback(() => {
522629
storeSplitTerminal(threadRef, `terminal-${randomUUID()}`);
523630
bumpFocusRequestId();
@@ -615,7 +722,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
615722
<div
616723
className={cn(
617724
"fixed inset-0 z-50 bg-black/32 backdrop-blur-sm",
618-
visible ? "grid grid-rows-[1fr_auto_3fr] justify-items-center p-4" : "hidden",
725+
visible ? "flex items-center justify-center p-3" : "hidden",
619726
)}
620727
onMouseDown={(event) => {
621728
if (event.target === event.currentTarget) {
@@ -627,8 +734,25 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
627734
role="dialog"
628735
aria-modal="true"
629736
aria-labelledby={floatingTerminalTitleId}
630-
className="row-start-2 w-[min(96vw,72rem)] max-w-[min(96vw,72rem)] overflow-hidden rounded-lg border bg-background p-0 shadow-xl"
737+
className="relative overflow-hidden rounded-lg border bg-background p-0 shadow-xl"
738+
style={{ width: `${floatingWidth}px` }}
631739
>
740+
{/* Left resize handle */}
741+
<div
742+
className="absolute inset-y-0 left-0 z-20 w-1.5 cursor-col-resize"
743+
onPointerDown={handleWidthResizePointerDownLeft}
744+
onPointerMove={handleWidthResizePointerMove}
745+
onPointerUp={handleWidthResizePointerEnd}
746+
onPointerCancel={handleWidthResizePointerEnd}
747+
/>
748+
{/* Right resize handle */}
749+
<div
750+
className="absolute inset-y-0 right-0 z-20 w-1.5 cursor-col-resize"
751+
onPointerDown={handleWidthResizePointerDownRight}
752+
onPointerMove={handleWidthResizePointerMove}
753+
onPointerUp={handleWidthResizePointerEnd}
754+
onPointerCancel={handleWidthResizePointerEnd}
755+
/>
632756
<div className="flex h-8 shrink-0 items-center justify-between border-b border-border/80 px-2">
633757
<h2 id={floatingTerminalTitleId} className="text-xs font-medium leading-none">
634758
Terminal

apps/web/src/components/ThreadTerminalDrawer.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,18 @@ import { readLocalApi } from "~/localApi";
5252
import { selectTerminalEventEntries, useTerminalStateStore } from "../terminalStateStore";
5353

5454
const MIN_DRAWER_HEIGHT = 180;
55-
const MAX_DRAWER_HEIGHT_RATIO = 0.75;
55+
const DEFAULT_MAX_DRAWER_HEIGHT_RATIO = 0.75;
56+
const FLOATING_MAX_DRAWER_HEIGHT_RATIO = 0.92;
5657
const MULTI_CLICK_SELECTION_ACTION_DELAY_MS = 260;
5758

58-
function maxDrawerHeight(): number {
59+
function maxDrawerHeight(ratio = DEFAULT_MAX_DRAWER_HEIGHT_RATIO): number {
5960
if (typeof window === "undefined") return DEFAULT_THREAD_TERMINAL_HEIGHT;
60-
return Math.max(MIN_DRAWER_HEIGHT, Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO));
61+
return Math.max(MIN_DRAWER_HEIGHT, Math.floor(window.innerHeight * ratio));
6162
}
6263

63-
function clampDrawerHeight(height: number): number {
64+
function clampDrawerHeight(height: number, ratio = DEFAULT_MAX_DRAWER_HEIGHT_RATIO): number {
6465
const safeHeight = Number.isFinite(height) ? height : DEFAULT_THREAD_TERMINAL_HEIGHT;
65-
const maxHeight = maxDrawerHeight();
66+
const maxHeight = maxDrawerHeight(ratio);
6667
return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight);
6768
}
6869

@@ -880,10 +881,14 @@ export default function ThreadTerminalDrawer({
880881
keybindings,
881882
layout = "docked",
882883
}: ThreadTerminalDrawerProps) {
883-
const [drawerHeight, setDrawerHeight] = useState(() => clampDrawerHeight(height));
884+
const maxHeightRatio =
885+
layout === "floating" ? FLOATING_MAX_DRAWER_HEIGHT_RATIO : DEFAULT_MAX_DRAWER_HEIGHT_RATIO;
886+
const maxHeightRatioRef = useRef(maxHeightRatio);
887+
maxHeightRatioRef.current = maxHeightRatio;
888+
const [drawerHeight, setDrawerHeight] = useState(() => clampDrawerHeight(height, maxHeightRatio));
884889
const [resizeEpoch, setResizeEpoch] = useState(0);
885890
const drawerHeightRef = useRef(drawerHeight);
886-
const lastSyncedHeightRef = useRef(clampDrawerHeight(height));
891+
const lastSyncedHeightRef = useRef(clampDrawerHeight(height, maxHeightRatio));
887892
const onHeightChangeRef = useRef(onHeightChange);
888893
const resizeStateRef = useRef<{
889894
pointerId: number;
@@ -1020,14 +1025,14 @@ export default function ThreadTerminalDrawer({
10201025
}, [drawerHeight]);
10211026

10221027
const syncHeight = useCallback((nextHeight: number) => {
1023-
const clampedHeight = clampDrawerHeight(nextHeight);
1028+
const clampedHeight = clampDrawerHeight(nextHeight, maxHeightRatioRef.current);
10241029
if (lastSyncedHeightRef.current === clampedHeight) return;
10251030
lastSyncedHeightRef.current = clampedHeight;
10261031
onHeightChangeRef.current(clampedHeight);
10271032
}, []);
10281033

10291034
useEffect(() => {
1030-
const clampedHeight = clampDrawerHeight(height);
1035+
const clampedHeight = clampDrawerHeight(height, maxHeightRatioRef.current);
10311036
setDrawerHeight(clampedHeight);
10321037
drawerHeightRef.current = clampedHeight;
10331038
lastSyncedHeightRef.current = clampedHeight;
@@ -1051,6 +1056,7 @@ export default function ThreadTerminalDrawer({
10511056
event.preventDefault();
10521057
const clampedHeight = clampDrawerHeight(
10531058
resizeState.startHeight + (resizeState.startY - event.clientY),
1059+
maxHeightRatioRef.current,
10541060
);
10551061
if (clampedHeight === drawerHeightRef.current) {
10561062
return;
@@ -1083,7 +1089,7 @@ export default function ThreadTerminalDrawer({
10831089
}
10841090

10851091
const onWindowResize = () => {
1086-
const clampedHeight = clampDrawerHeight(drawerHeightRef.current);
1092+
const clampedHeight = clampDrawerHeight(drawerHeightRef.current, maxHeightRatioRef.current);
10871093
const changed = clampedHeight !== drawerHeightRef.current;
10881094
if (changed) {
10891095
setDrawerHeight(clampedHeight);

apps/web/src/components/settings/SettingsPanels.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,20 +106,6 @@ const TERMINAL_LAYOUT_LABELS = {
106106
floating: "Floating",
107107
} as const;
108108

109-
type InstallProviderSettings = {
110-
provider: ProviderKind;
111-
title: string;
112-
badgeLabel?: string;
113-
binaryPlaceholder: string;
114-
binaryDescription: ReactNode;
115-
serverUrlPlaceholder?: string;
116-
serverUrlDescription?: ReactNode;
117-
serverPasswordPlaceholder?: string;
118-
serverPasswordDescription?: ReactNode;
119-
homePathKey?: "codexHomePath";
120-
homePlaceholder?: string;
121-
homeDescription?: ReactNode;
122-
};
123109

124110
function withoutProviderInstanceKey<V>(
125111
record: Readonly<Record<ProviderInstanceId, V>> | undefined,

apps/web/src/terminalStateStore.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { resolveStorage } from "./lib/storage";
1313
import { terminalRunningSubprocessFromEvent } from "./terminalActivity";
1414
import {
1515
DEFAULT_THREAD_TERMINAL_HEIGHT,
16+
DEFAULT_THREAD_TERMINAL_WIDTH,
1617
DEFAULT_THREAD_TERMINAL_ID,
1718
MAX_TERMINALS_PER_GROUP,
1819
type ThreadTerminalGroup,
@@ -21,6 +22,7 @@ import {
2122
interface ThreadTerminalState {
2223
terminalOpen: boolean;
2324
terminalHeight: number;
25+
terminalWidth: number;
2426
terminalIds: string[];
2527
runningTerminalIds: string[];
2628
activeTerminalId: string;
@@ -181,6 +183,7 @@ function threadTerminalStateEqual(left: ThreadTerminalState, right: ThreadTermin
181183
return (
182184
left.terminalOpen === right.terminalOpen &&
183185
left.terminalHeight === right.terminalHeight &&
186+
left.terminalWidth === right.terminalWidth &&
184187
left.activeTerminalId === right.activeTerminalId &&
185188
left.activeTerminalGroupId === right.activeTerminalGroupId &&
186189
arraysEqual(left.terminalIds, right.terminalIds) &&
@@ -192,6 +195,7 @@ function threadTerminalStateEqual(left: ThreadTerminalState, right: ThreadTermin
192195
const DEFAULT_THREAD_TERMINAL_STATE: ThreadTerminalState = Object.freeze({
193196
terminalOpen: false,
194197
terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT,
198+
terminalWidth: DEFAULT_THREAD_TERMINAL_WIDTH,
195199
terminalIds: [DEFAULT_THREAD_TERMINAL_ID],
196200
runningTerminalIds: [],
197201
activeTerminalId: DEFAULT_THREAD_TERMINAL_ID,
@@ -239,6 +243,10 @@ function normalizeThreadTerminalState(state: ThreadTerminalState): ThreadTermina
239243
Number.isFinite(state.terminalHeight) && state.terminalHeight > 0
240244
? state.terminalHeight
241245
: DEFAULT_THREAD_TERMINAL_HEIGHT,
246+
terminalWidth:
247+
Number.isFinite(state.terminalWidth) && state.terminalWidth > 0
248+
? state.terminalWidth
249+
: DEFAULT_THREAD_TERMINAL_WIDTH,
242250
terminalIds: nextTerminalIds,
243251
runningTerminalIds,
244252
activeTerminalId,
@@ -413,6 +421,14 @@ function setThreadTerminalHeight(state: ThreadTerminalState, height: number): Th
413421
return { ...normalized, terminalHeight: height };
414422
}
415423

424+
function setThreadTerminalWidth(state: ThreadTerminalState, width: number): ThreadTerminalState {
425+
const normalized = normalizeThreadTerminalState(state);
426+
if (!Number.isFinite(width) || width <= 0 || normalized.terminalWidth === width) {
427+
return normalized;
428+
}
429+
return { ...normalized, terminalWidth: width };
430+
}
431+
416432
function splitThreadTerminal(state: ThreadTerminalState, terminalId: string): ThreadTerminalState {
417433
return upsertTerminalIntoGroups(state, terminalId, "split");
418434
}
@@ -479,6 +495,7 @@ function closeThreadTerminal(state: ThreadTerminalState, terminalId: string): Th
479495
return normalizeThreadTerminalState({
480496
terminalOpen: normalized.terminalOpen,
481497
terminalHeight: normalized.terminalHeight,
498+
terminalWidth: normalized.terminalWidth,
482499
terminalIds: remainingTerminalIds,
483500
runningTerminalIds: normalized.runningTerminalIds.filter((id) => id !== terminalId),
484501
activeTerminalId: nextActiveTerminalId,
@@ -570,6 +587,7 @@ interface TerminalStateStoreState {
570587
nextTerminalEventId: number;
571588
setTerminalOpen: (threadRef: ScopedThreadRef, open: boolean) => void;
572589
setTerminalHeight: (threadRef: ScopedThreadRef, height: number) => void;
590+
setTerminalWidth: (threadRef: ScopedThreadRef, width: number) => void;
573591
splitTerminal: (threadRef: ScopedThreadRef, terminalId: string) => void;
574592
newTerminal: (threadRef: ScopedThreadRef, terminalId: string) => void;
575593
ensureTerminal: (
@@ -627,6 +645,8 @@ export const useTerminalStateStore = create<TerminalStateStoreState>()(
627645
updateTerminal(threadRef, (state) => setThreadTerminalOpen(state, open)),
628646
setTerminalHeight: (threadRef, height) =>
629647
updateTerminal(threadRef, (state) => setThreadTerminalHeight(state, height)),
648+
setTerminalWidth: (threadRef, width) =>
649+
updateTerminal(threadRef, (state) => setThreadTerminalWidth(state, width)),
630650
splitTerminal: (threadRef, terminalId) =>
631651
updateTerminal(threadRef, (state) => splitThreadTerminal(state, terminalId)),
632652
newTerminal: (threadRef, terminalId) =>

apps/web/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
2323

2424
export const DEFAULT_INTERACTION_MODE: ProviderInteractionMode = "default";
2525
export const DEFAULT_THREAD_TERMINAL_HEIGHT = 280;
26+
export const DEFAULT_THREAD_TERMINAL_WIDTH = 900;
2627
export const DEFAULT_THREAD_TERMINAL_ID = "default";
2728
export const MAX_TERMINALS_PER_GROUP = 4;
2829
export type ProjectScript = ContractProjectScript;

0 commit comments

Comments
 (0)