Skip to content
Closed
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
2 changes: 2 additions & 0 deletions KEYBINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ See the full schema for more details: [`packages/contracts/src/keybindings.ts`](
{ "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" },
{ "key": "mod+w", "command": "terminal.close", "when": "terminalFocus" },
{ "key": "mod+k", "command": "commandPalette.toggle", "when": "!terminalFocus" },
{ "key": "mod+b", "command": "sidebar.toggle", "when": "!terminalFocus" },
{ "key": "mod+n", "command": "chat.new", "when": "!terminalFocus" },
{ "key": "mod+shift+o", "command": "chat.new", "when": "!terminalFocus" },
{ "key": "mod+shift+n", "command": "chat.newLocal", "when": "!terminalFocus" },
Expand Down Expand Up @@ -52,6 +53,7 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged
- `terminal.new`: create new terminal (in focused terminal context by default)
- `terminal.close`: close/kill the focused terminal (in focused terminal context by default)
- `commandPalette.toggle`: open or close the global command palette
- `sidebar.toggle`: open or close the thread sidebar
- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state
- `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`))
- `editor.openFavorite`: open current project/worktree in the last-used editor
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/components/AppSidebarLayout.logic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";

import { canCollapseAppSidebar } from "./AppSidebarLayout.logic";

describe("canCollapseAppSidebar", () => {
it("allows collapse in chat/editor routes", () => {
expect(canCollapseAppSidebar("/")).toBe(true);
expect(canCollapseAppSidebar("/environment-local/thread-123")).toBe(true);
});

it("keeps settings routes expanded", () => {
expect(canCollapseAppSidebar("/settings")).toBe(false);
expect(canCollapseAppSidebar("/settings/general")).toBe(false);
});
});
3 changes: 3 additions & 0 deletions apps/web/src/components/AppSidebarLayout.logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function canCollapseAppSidebar(pathname: string): boolean {
return !pathname.startsWith("/settings");
}
15 changes: 9 additions & 6 deletions apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, type ReactNode } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useLocation, useNavigate } from "@tanstack/react-router";

import ThreadSidebar from "./Sidebar";
import { Sidebar, SidebarProvider, SidebarRail } from "./ui/sidebar";
import { Sidebar, SidebarRail } from "./ui/sidebar";
import { canCollapseAppSidebar } from "./AppSidebarLayout.logic";
import {
clearShortcutModifierState,
syncShortcutModifierStateFromKeyboardEvent,
Expand All @@ -13,6 +14,8 @@ const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;
const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16;
export function AppSidebarLayout({ children }: { children: ReactNode }) {
const navigate = useNavigate();
const pathname = useLocation({ select: (location) => location.pathname });
const sidebarCanCollapse = canCollapseAppSidebar(pathname);

useEffect(() => {
const onWindowKeyDown = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -54,10 +57,10 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
}, [navigate]);

return (
<SidebarProvider className="h-dvh! min-h-0!" defaultOpen>
<>
<Sidebar
side="left"
collapsible="offcanvas"
collapsible={sidebarCanCollapse ? "offcanvas" : "none"}
className="border-r border-border bg-card text-foreground"
resizable={{
minWidth: THREAD_SIDEBAR_MIN_WIDTH,
Expand All @@ -67,9 +70,9 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
}}
>
<ThreadSidebar />
<SidebarRail />
{sidebarCanCollapse ? <SidebarRail /> : null}
</Sidebar>
{children}
</SidebarProvider>
</>
);
}
12 changes: 12 additions & 0 deletions apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { afterEach, describe, expect, it, vi } from "vitest";
import { type EnvironmentState, useStore } from "../store";
import { type Thread } from "../types";
import { INLINE_DIFF_CONTEXT_COMMENT_PLACEHOLDER } from "../lib/diffContextComments";

import {
MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,
Expand Down Expand Up @@ -72,6 +73,17 @@ describe("deriveComposerSendState", () => {
expect(state.expiredTerminalContextCount).toBe(1);
expect(state.hasSendableContent).toBe(true);
});

it("strips diff comment placeholders from visible prompt text", () => {
const state = deriveComposerSendState({
prompt: INLINE_DIFF_CONTEXT_COMMENT_PLACEHOLDER,
imageCount: 0,
terminalContexts: [],
});

expect(state.trimmedPrompt).toBe("");
expect(state.hasSendableContent).toBe(false);
});
});

describe("buildExpiredTerminalContextToastCopy", () => {
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
stripInlineTerminalContextPlaceholders,
type TerminalContextDraft,
} from "../lib/terminalContext";
import { stripInlineDiffContextCommentPlaceholders } from "../lib/diffContextComments";
import type { DraftThreadEnvMode } from "../composerDraftStore";

export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";
Expand Down Expand Up @@ -190,7 +191,9 @@ export function deriveComposerSendState(options: {
expiredTerminalContextCount: number;
hasSendableContent: boolean;
} {
const trimmedPrompt = stripInlineTerminalContextPlaceholders(options.prompt).trim();
const trimmedPrompt = stripInlineDiffContextCommentPlaceholders(
stripInlineTerminalContextPlaceholders(options.prompt),
).trim();
const sendableTerminalContexts = filterTerminalContextsWithText(options.terminalContexts);
const expiredTerminalContextCount =
options.terminalContexts.length - sendableTerminalContexts.length;
Expand Down
45 changes: 33 additions & 12 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ import { buildDraftThreadRouteParams } from "../threadRoutes";
import {
type ComposerImageAttachment,
type DraftThreadEnvMode,
flushComposerDraftStorage,
useComposerDraftStore,
type DraftId,
} from "../composerDraftStore";
Expand All @@ -141,6 +142,7 @@ import {
type TerminalContextDraft,
type TerminalContextSelection,
} from "../lib/terminalContext";
import { appendDiffContextCommentsToPrompt } from "../lib/diffContextComments";
import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
import { ChatComposer, type ChatComposerHandle } from "./chat/ChatComposer";
import { ExpandedImageDialog } from "./chat/ExpandedImageDialog";
Expand Down Expand Up @@ -653,17 +655,15 @@ export default function ChatView(props: ChatViewProps) {
const composerActiveProvider = useComposerDraftStore(
(store) => store.getComposerDraft(composerDraftTarget)?.activeProvider ?? null,
);
const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt);
const addComposerDraftImages = useComposerDraftStore((store) => store.addImages);
const setComposerDraftTerminalContexts = useComposerDraftStore(
(store) => store.setTerminalContexts,
);
const setComposerDraftModelSelection = useComposerDraftStore((store) => store.setModelSelection);
const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode);
const setComposerDraftInteractionMode = useComposerDraftStore(
(store) => store.setInteractionMode,
);
const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent);
const restoreComposerDraftSendContent = useComposerDraftStore(
(store) => store.restoreComposerSendContent,
);
const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
const getDraftSessionByLogicalProjectKey = useComposerDraftStore(
(store) => store.getDraftSessionByLogicalProjectKey,
Expand Down Expand Up @@ -2628,7 +2628,9 @@ export default function ChatView(props: ChatViewProps) {
if (!sendCtx) return;
const {
images: composerImages,
persistedAttachments: composerPersistedAttachments,
terminalContexts: composerTerminalContexts,
diffContextComments: composerDiffContextComments,
selectedProvider: ctxSelectedProvider,
selectedModel: ctxSelectedModel,
selectedProviderModels: ctxSelectedProviderModels,
Expand Down Expand Up @@ -2661,7 +2663,9 @@ export default function ChatView(props: ChatViewProps) {
return;
}
const standaloneSlashCommand =
composerImages.length === 0 && sendableComposerTerminalContexts.length === 0
composerImages.length === 0 &&
sendableComposerTerminalContexts.length === 0 &&
composerDiffContextComments.length === 0
? parseStandaloneComposerSlashCommand(trimmed)
: null;
if (standaloneSlashCommand) {
Expand All @@ -2671,7 +2675,8 @@ export default function ChatView(props: ChatViewProps) {
composerRef.current?.resetCursorState();
return;
}
if (!hasSendableContent) {
const hasPendingDiffContextComments = composerDiffContextComments.length > 0;
if (!hasSendableContent && !hasPendingDiffContextComments) {
if (expiredTerminalContextCount > 0) {
const toastCopy = buildExpiredTerminalContextToastCopy(
expiredTerminalContextCount,
Expand Down Expand Up @@ -2708,11 +2713,17 @@ export default function ChatView(props: ChatViewProps) {
beginLocalDispatch({ preparingWorktree: Boolean(baseBranchForWorktree) });

const composerImagesSnapshot = [...composerImages];
const composerPersistedAttachmentsSnapshot = [...composerPersistedAttachments];
const composerTerminalContextsSnapshot = [...sendableComposerTerminalContexts];
const messageTextForSend = appendTerminalContextsToPrompt(
const composerDiffContextCommentsSnapshot = [...composerDiffContextComments];
const messageTextWithTerminalContexts = appendTerminalContextsToPrompt(
promptForSend,
composerTerminalContextsSnapshot,
);
const messageTextForSend = appendDiffContextCommentsToPrompt(
messageTextWithTerminalContexts,
composerDiffContextCommentsSnapshot,
);
const messageIdForSend = newMessageId();
const messageCreatedAt = new Date().toISOString();
const outgoingMessageText = formatOutgoingPrompt({
Expand Down Expand Up @@ -2775,6 +2786,7 @@ export default function ChatView(props: ChatViewProps) {
}
promptRef.current = "";
clearComposerDraftContent(composerDraftTarget);
flushComposerDraftStorage();
composerRef.current?.resetCursorState();

let turnStartSucceeded = false;
Expand Down Expand Up @@ -2877,7 +2889,9 @@ export default function ChatView(props: ChatViewProps) {
!turnStartSucceeded &&
promptRef.current.length === 0 &&
composerImagesRef.current.length === 0 &&
composerTerminalContextsRef.current.length === 0
composerTerminalContextsRef.current.length === 0 &&
(useComposerDraftStore.getState().getComposerDraft(composerDraftTarget)?.diffContextComments
.length ?? 0) === 0
) {
setOptimisticUserMessages((existing) => {
const removed = existing.filter((message) => message.id === messageIdForSend);
Expand All @@ -2891,9 +2905,14 @@ export default function ChatView(props: ChatViewProps) {
const retryComposerImages = composerImagesSnapshot.map(cloneComposerImageForRetry);
composerImagesRef.current = retryComposerImages;
composerTerminalContextsRef.current = composerTerminalContextsSnapshot;
setComposerDraftPrompt(composerDraftTarget, promptForSend);
addComposerDraftImages(composerDraftTarget, retryComposerImages);
setComposerDraftTerminalContexts(composerDraftTarget, composerTerminalContextsSnapshot);
restoreComposerDraftSendContent(composerDraftTarget, {
prompt: promptForSend,
images: retryComposerImages,
persistedAttachments: composerPersistedAttachmentsSnapshot,
terminalContexts: composerTerminalContextsSnapshot,
diffContextComments: composerDiffContextCommentsSnapshot,
});
flushComposerDraftStorage();
composerRef.current?.resetCursorState({
cursor: collapseExpandedComposerCursor(promptForSend, promptForSend.length),
prompt: promptForSend,
Expand Down Expand Up @@ -3505,6 +3524,8 @@ export default function ChatView(props: ChatViewProps) {
isElectron
? cn(
"drag-region flex h-[52px] items-center px-3 sm:px-5 wco:h-[env(titlebar-area-height)]",
"transition-[padding-left] duration-200 ease-linear",
"group-data-[state=collapsed]/sidebar-wrapper:pl-[90px] group-data-[state=collapsed]/sidebar-wrapper:wco:pl-[calc(env(titlebar-area-x)+1em)]",
reserveTitleBarControlInset &&
"wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
)
Expand Down
22 changes: 21 additions & 1 deletion apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type SourceControlRepositoryInfo,
} from "@t3tools/contracts";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useNavigate, useParams } from "@tanstack/react-router";
import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
import * as Option from "effect/Option";
import {
ArrowDownIcon,
Expand All @@ -23,6 +23,7 @@ import {
FolderPlusIcon,
LinkIcon,
MessageSquareIcon,
PanelLeftIcon,
SettingsIcon,
SquarePenIcon,
} from "lucide-react";
Expand Down Expand Up @@ -115,9 +116,11 @@ import {
import { Button } from "./ui/button";
import { Kbd, KbdGroup } from "./ui/kbd";
import { stackedThreadToast, toastManager } from "./ui/toast";
import { useSidebar } from "./ui/sidebar";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext";
import type { ChatComposerHandle } from "./chat/ChatComposer";
import { canCollapseAppSidebar } from "./AppSidebarLayout.logic";

const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = [];
const BROWSE_STALE_TIME_MS = 30_000;
Expand Down Expand Up @@ -392,6 +395,8 @@ function CommandPaletteDialog() {

function OpenCommandPaletteDialog() {
const navigate = useNavigate();
const pathname = useLocation({ select: (location) => location.pathname });
const { toggleSidebar } = useSidebar();
const setOpen = useCommandPaletteStore((store) => store.setOpen);
const openIntent = useCommandPaletteStore((store) => store.openIntent);
const clearOpenIntent = useCommandPaletteStore((store) => store.clearOpenIntent);
Expand All @@ -402,6 +407,7 @@ function OpenCommandPaletteDialog() {
const queryClient = useQueryClient();
const [highlightedItemValue, setHighlightedItemValue] = useState<string | null>(null);
const settings = useSettings();
const sidebarCanCollapse = canCollapseAppSidebar(pathname);
const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread } =
useHandleNewThread();
const projects = useStore(useShallow(selectProjectsAcrossEnvironments));
Expand Down Expand Up @@ -1050,6 +1056,20 @@ function OpenCommandPaletteDialog() {
},
});

if (sidebarCanCollapse) {
actionItems.push({
kind: "action",
value: "action:toggle-sidebar",
searchTerms: ["sidebar", "toggle", "collapse", "hide", "show"],
title: "Toggle sidebar",
icon: <PanelLeftIcon className={ITEM_ICON_CLASS} />,
shortcutCommand: "sidebar.toggle",
run: async () => {
toggleSidebar();
},
});
}

actionItems.push({
kind: "action",
value: "action:settings",
Expand Down
Loading
Loading