Skip to content

Commit a485c6a

Browse files
committed
feat(web): improve terminal workspace controls
1 parent 696a2e1 commit a485c6a

6 files changed

Lines changed: 778 additions & 368 deletions

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,11 @@ import {
221221
import { appendPreviewAnnotationPrompt } from "../lib/previewAnnotation";
222222
import { appendReviewCommentsToPrompt, type ReviewCommentContext } from "../reviewCommentContext";
223223
import { environmentCatalog } from "../connection/catalog";
224-
import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore";
224+
import {
225+
selectThreadTerminalCustomLabels,
226+
selectThreadTerminalUiState,
227+
useTerminalUiStateStore,
228+
} from "../terminalUiStateStore";
225229
import { useKnownTerminalSessions, useThreadRunningTerminalIds } from "../state/terminalSessions";
226230
import { projectEnvironment } from "../state/projects";
227231
import { useEnvironmentQuery } from "../state/query";
@@ -658,6 +662,7 @@ interface PersistentThreadTerminalDrawerProps {
658662
newShortcutLabel: string | undefined;
659663
closeShortcutLabel: string | undefined;
660664
keybindings: ResolvedKeybindingsConfig;
665+
onHide: () => void;
661666
onAddTerminalContext: (selection: TerminalContextSelection) => void;
662667
}
663668

@@ -672,6 +677,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
672677
newShortcutLabel,
673678
closeShortcutLabel,
674679
keybindings,
680+
onHide,
675681
onAddTerminalContext,
676682
}: PersistentThreadTerminalDrawerProps) {
677683
const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open");
@@ -787,8 +793,14 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
787793
) {
788794
return;
789795
}
790-
reconcileTerminalIds(threadRef, serverOrderedTerminalIds);
791-
}, [reconcileTerminalIds, serverOrderedTerminalIds, terminalUiState.terminalIds, threadRef]);
796+
reconcileTerminalIds(threadRef, serverOrderedTerminalIds, [...panelTerminalIds]);
797+
}, [
798+
panelTerminalIds,
799+
reconcileTerminalIds,
800+
serverOrderedTerminalIds,
801+
terminalUiState.terminalIds,
802+
threadRef,
803+
]);
792804
const [localFocusRequestId, setLocalFocusRequestId] = useState(0);
793805
const worktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null;
794806
const effectiveWorktreePath = useMemo(() => {
@@ -995,6 +1007,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
9951007
onSplitTerminal={splitTerminal}
9961008
onSplitTerminalVertical={splitTerminalVertical}
9971009
onNewTerminal={createNewTerminal}
1010+
onHide={onHide}
9981011
splitShortcutLabel={visible ? splitShortcutLabel : undefined}
9991012
splitVerticalShortcutLabel={visible ? splitVerticalShortcutLabel : undefined}
10001013
newShortcutLabel={visible ? newShortcutLabel : undefined}
@@ -1557,6 +1570,16 @@ function ChatViewContent(props: ChatViewProps) {
15571570
const canCheckoutPullRequestIntoThread = isLocalDraftThread;
15581571
const activeThreadId = activeThread?.id ?? null;
15591572
const activeThreadEnvironmentId = activeThread?.environmentId ?? null;
1573+
const activeThreadRef = useMemo(
1574+
() =>
1575+
activeThreadEnvironmentId && activeThreadId
1576+
? scopeThreadRef(activeThreadEnvironmentId, activeThreadId)
1577+
: null,
1578+
[activeThreadEnvironmentId, activeThreadId],
1579+
);
1580+
const activeTerminalCustomLabels = useTerminalUiStateStore((state) =>
1581+
selectThreadTerminalCustomLabels(state.terminalCustomLabelsByThreadKey, activeThreadRef),
1582+
);
15601583
const runningTerminalIds = useThreadRunningTerminalIds({
15611584
environmentId: activeThread?.environmentId ?? null,
15621585
threadId: activeThreadId,
@@ -1586,18 +1609,15 @@ function ChatViewContent(props: ChatViewProps) {
15861609
for (const session of activeThreadKnownSessions) {
15871610
labels.set(
15881611
session.target.terminalId,
1589-
resolveTerminalSessionLabel(session.target.terminalId, session.state.summary),
1612+
activeTerminalCustomLabels[session.target.terminalId] ??
1613+
resolveTerminalSessionLabel(session.target.terminalId, session.state.summary),
15901614
);
15911615
}
1616+
for (const [terminalId, label] of Object.entries(activeTerminalCustomLabels)) {
1617+
if (!labels.has(terminalId)) labels.set(terminalId, label);
1618+
}
15921619
return labels;
1593-
}, [activeThreadKnownSessions]);
1594-
const activeThreadRef = useMemo(
1595-
() =>
1596-
activeThreadEnvironmentId && activeThreadId
1597-
? scopeThreadRef(activeThreadEnvironmentId, activeThreadId)
1598-
: null,
1599-
[activeThreadEnvironmentId, activeThreadId],
1600-
);
1620+
}, [activeTerminalCustomLabels, activeThreadKnownSessions]);
16011621
const activeThreadKey = activeThreadRef ? scopedThreadKey(activeThreadRef) : null;
16021622
const changeRequestSnapshotByKey = useAtomValue(threadChangeRequestSnapshotsAtom);
16031623
const [timelineAnchor, setTimelineAnchor] = useState<{
@@ -2826,6 +2846,7 @@ function ChatViewContent(props: ChatViewProps) {
28262846
},
28272847
[activeThreadRef, storeSetTerminalOpen],
28282848
);
2849+
const hideTerminal = useCallback(() => setTerminalOpen(false), [setTerminalOpen]);
28292850
const toggleTerminalVisibility = useCallback(() => {
28302851
if (!activeThreadRef) return;
28312852
const nextOpen = !terminalUiState.terminalOpen;
@@ -6603,6 +6624,7 @@ function ChatViewContent(props: ChatViewProps) {
66036624
newShortcutLabel={newTerminalShortcutLabel ?? undefined}
66046625
closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
66056626
keybindings={keybindings}
6627+
onHide={hideTerminal}
66066628
onAddTerminalContext={addTerminalContextToDraft}
66076629
/>
66086630
))}

apps/web/src/components/ThreadTerminalDrawer.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,49 @@
11
import { describe, expect, it } from "vite-plus/test";
22

33
import {
4+
getTerminalSidebarMaxWidth,
5+
resolveTerminalCustomLabelAfterRename,
46
resolveTerminalSelectionActionPosition,
57
shouldHandleTerminalExit,
68
shouldHandleTerminalSelectionMouseUp,
9+
shouldShowTerminalSidebar,
710
terminalSelectionActionDelayForClickCount,
811
terminalSelectionLineRange,
912
} from "./ThreadTerminalDrawer";
1013

14+
describe("resolveTerminalCustomLabelAfterRename", () => {
15+
it("does not persist an untouched automatic label after it changes", () => {
16+
expect(resolveTerminalCustomLabelAfterRename("bash", null, "bash", "vite")).toBeNull();
17+
});
18+
19+
it("keeps an intentional custom label", () => {
20+
expect(resolveTerminalCustomLabelAfterRename("server", null, "bash", "vite")).toBe("server");
21+
});
22+
23+
it("keeps an untouched custom label when the automatic label changes to match it", () => {
24+
expect(resolveTerminalCustomLabelAfterRename("server", "server", "bash", "server")).toBe(
25+
"server",
26+
);
27+
});
28+
});
29+
30+
describe("getTerminalSidebarMaxWidth", () => {
31+
it("reserves usable terminal space in a narrow panel", () => {
32+
expect(getTerminalSidebarMaxWidth()).toBe(320);
33+
expect(getTerminalSidebarMaxWidth(360)).toBe(144);
34+
expect(getTerminalSidebarMaxWidth(480)).toBe(264);
35+
expect(getTerminalSidebarMaxWidth(800)).toBe(320);
36+
});
37+
});
38+
39+
describe("shouldShowTerminalSidebar", () => {
40+
it("shows the sidebar only when there is more than one terminal", () => {
41+
expect(shouldShowTerminalSidebar(0)).toBe(false);
42+
expect(shouldShowTerminalSidebar(1)).toBe(false);
43+
expect(shouldShowTerminalSidebar(2)).toBe(true);
44+
});
45+
});
46+
1147
describe("resolveTerminalSelectionActionPosition", () => {
1248
it("prefers the selection rect over the last pointer position", () => {
1349
expect(

0 commit comments

Comments
 (0)