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 apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const clientSettings: ClientSettings = {
fontSizeTerminal: 12,
fontSmoothing: true,
glassOpacity: 80,
newThreadOpenFilesPanel: false,
newThreadOpenTerminal: false,
planModeEnabled: false,
providerModelPreferences: {},
sidebarAutoSettleAfterDays: 3,
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ import {
} from "../hooks/useSettings";
import { useNowMinute } from "../hooks/useNowMinute";
import { useNewThreadHandler } from "../hooks/useHandleNewThread";
import { applyNewThreadPanelDefaults } from "../newThreadPanelDefaults";
import { resolveAppModelSelectionForInstance } from "../modelSelection";
import { getTerminalFocusOwner } from "../lib/terminalFocus";
import { preventRepeatedTerminalCloseShortcut } from "../lib/terminalCloseShortcut";
Expand Down Expand Up @@ -1885,6 +1886,12 @@ function ChatViewContent(props: ChatViewProps) {
throw new Error("No active project is available for this pull request.");
}
const activeProjectRef = scopeProjectRef(activeProject.environmentId, activeProject.id);
// Each branch below hands the user a new chat in this project, so each one
// opens with the configured panel layout, exactly like the new-thread
// handler. A chat that already has a layout keeps it.
const applyPanelDefaults = (threadId: ThreadId) => {
void applyNewThreadPanelDefaults(scopeThreadRef(activeProjectRef.environmentId, threadId));
};
const logicalProjectKey = deriveLogicalProjectKeyFromSettings(
activeProject,
projectGroupingSettings,
Expand All @@ -1901,6 +1908,7 @@ function ChatViewContent(props: ChatViewProps) {
...input,
},
);
applyPanelDefaults(storedDraftSession.threadId);
if (routeKind !== "draft" || draftId !== storedDraftSession.draftId) {
await navigate({
to: "/draft/$draftId",
Expand All @@ -1924,6 +1932,7 @@ function ChatViewContent(props: ChatViewProps) {
interactionMode: activeDraftSession.interactionMode,
...input,
});
applyPanelDefaults(activeDraftSession.threadId);
return activeDraftSession.threadId;
}

Expand All @@ -1936,6 +1945,7 @@ function ChatViewContent(props: ChatViewProps) {
interactionMode: DEFAULT_INTERACTION_MODE,
...input,
});
applyPanelDefaults(nextThreadId);
await navigate({
to: "/draft/$draftId",
params: buildDraftThreadRouteParams(nextDraftId),
Expand Down Expand Up @@ -5745,6 +5755,9 @@ function ChatViewContent(props: ChatViewProps) {
}

if (failure === null) {
// The implementation runs in a chat that did not exist a moment ago, so it
// opens with the configured panel layout like any other new chat.
void applyNewThreadPanelDefaults(scopeThreadRef(activeThread.environmentId, nextThreadId));
const navigateResult = await settlePromise(() =>
navigate({
to: "/$environmentId/$threadId",
Expand Down
63 changes: 63 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,12 @@ export function useSettingsRestore(onRestored?: () => void) {
DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin
? ["New worktrees start from origin"]
: []),
...(settings.newThreadOpenFilesPanel !== DEFAULT_UNIFIED_SETTINGS.newThreadOpenFilesPanel
? ["Open files panel in new chats"]
: []),
...(settings.newThreadOpenTerminal !== DEFAULT_UNIFIED_SETTINGS.newThreadOpenTerminal
? ["Open terminal in new chats"]
: []),
...(settings.addProjectBaseDirectory !== DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory
? ["Add project base directory"]
: []),
Expand All @@ -532,6 +538,8 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.confirmThreadDelete,
settings.addProjectBaseDirectory,
settings.defaultThreadEnvMode,
settings.newThreadOpenFilesPanel,
settings.newThreadOpenTerminal,
settings.newWorktreesStartFromOrigin,
settings.diffIgnoreWhitespace,
settings.environmentIdentificationMode,
Expand Down Expand Up @@ -636,6 +644,8 @@ export function useSettingsRestore(onRestored?: () => void) {
providerHealthRefreshInterval: DEFAULT_UNIFIED_SETTINGS.providerHealthRefreshInterval,
defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode,
newWorktreesStartFromOrigin: DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin,
newThreadOpenFilesPanel: DEFAULT_UNIFIED_SETTINGS.newThreadOpenFilesPanel,
newThreadOpenTerminal: DEFAULT_UNIFIED_SETTINGS.newThreadOpenTerminal,
addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory,
confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive,
confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete,
Expand Down Expand Up @@ -2122,6 +2132,59 @@ export function GeneralSettingsPanel() {
/>
) : null}

<SettingsRow
{...searchableSetting("new-thread-open-files")}
description="Start every new chat in a project with the files panel already open."
Comment on lines +2135 to +2137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the new chat-layout settings

This exposes two user-visible settings that change the initial layout of every project chat, but a repo-wide search finds no corresponding addition under docs/user/. Add shipped-product documentation describing these preferences and their defaults so the behavior is documented in the required audience-specific location.

AGENTS.md reference: AGENTS.md:L75-L75

Useful? React with 👍 / 👎.

resetAction={
settings.newThreadOpenFilesPanel !==
DEFAULT_UNIFIED_SETTINGS.newThreadOpenFilesPanel ? (
<SettingResetButton
label="open files panel in new chats"
onClick={() =>
updateSettings({
newThreadOpenFilesPanel: DEFAULT_UNIFIED_SETTINGS.newThreadOpenFilesPanel,
})
}
/>
) : null
}
control={
<Switch
checked={settings.newThreadOpenFilesPanel}
onCheckedChange={(checked) =>
updateSettings({ newThreadOpenFilesPanel: Boolean(checked) })
}
aria-label="Open files panel in new chats"
/>
}
/>

<SettingsRow
{...searchableSetting("new-thread-open-terminal")}
description="Start every new chat in a project with a terminal already open."
resetAction={
settings.newThreadOpenTerminal !== DEFAULT_UNIFIED_SETTINGS.newThreadOpenTerminal ? (
<SettingResetButton
label="open terminal in new chats"
onClick={() =>
updateSettings({
newThreadOpenTerminal: DEFAULT_UNIFIED_SETTINGS.newThreadOpenTerminal,
})
}
/>
) : null
}
control={
<Switch
checked={settings.newThreadOpenTerminal}
onCheckedChange={(checked) =>
updateSettings({ newThreadOpenTerminal: Boolean(checked) })
}
aria-label="Open terminal in new chats"
/>
}
/>

<SettingsRow
{...searchableSetting("add-project-starts-in")}
description='Leave empty to use "~/" when the Add Project browser opens.'
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ export const SETTINGS_SEARCH_ITEMS = [
to: "/settings/general",
targetId: "new-threads",
},
{
id: "new-thread-open-files",
title: "Open files panel in new chats",
to: "/settings/general",
},
{
id: "new-thread-open-terminal",
title: "Open terminal in new chats",
to: "/settings/general",
},
{
id: "add-project-starts-in",
title: "Add project starts in",
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/hooks/useHandleNewThread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useComposerDraftStore,
} from "../composerDraftStore";
import { newDraftId, newThreadId } from "../lib/utils";
import { applyNewThreadPanelDefaults } from "../newThreadPanelDefaults";
import { orderItemsByPreferredIds } from "../components/Sidebar.logic";
import {
deriveLogicalProjectKeyFromSettings,
Expand Down Expand Up @@ -267,6 +268,13 @@ export function useNewThreadHandler() {
...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}),
},
);
// This draft is presented as a new chat, so it gets the configured
// default panel layout too — otherwise turning the preference on
// appears to do nothing until the project's next real thread. A
// draft that already has a layout keeps it.
void applyNewThreadPanelDefaults(
scopeThreadRef(projectRef.environmentId, emptyStoredDraftThread.threadId),
);
const opened = {
draftId: emptyStoredDraftThread.draftId,
threadId: emptyStoredDraftThread.threadId,
Expand Down Expand Up @@ -314,6 +322,11 @@ export function useNewThreadHandler() {
interactionMode: latestActiveDraftThread.interactionMode,
...pickExplicitWorkspaceOptions(options),
});
// Presented as a new chat like the reuse path above, so it gets the
// configured default panel layout too.
void applyNewThreadPanelDefaults(
scopeThreadRef(projectRef.environmentId, latestActiveDraftThread.threadId),
);
return Promise.resolve({
draftId: currentRouteTarget.draftId,
threadId: latestActiveDraftThread.threadId,
Expand Down Expand Up @@ -377,6 +390,7 @@ export function useNewThreadHandler() {
...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}),
});
applyStickyState(draftId);
void applyNewThreadPanelDefaults(scopeThreadRef(projectRef.environmentId, threadId));
if (carryModelSelection) {
// After sticky state so the viewed thread's exact selection
// (model + options like effort and context window) wins over the
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/hooks/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ export function getClientSettings(): ClientSettings {
return getClientSettingsSnapshot();
}

/**
* The client settings, once the persisted ones have landed. The snapshot reads
* as the defaults until hydration completes, so a one-shot decision taken at
* startup — rather than a subscription that re-renders on change — has to wait
* for this instead of reading {@link getClientSettings} directly.
*/
export async function getHydratedClientSettings(): Promise<ClientSettings> {
await hydrateClientSettings();
return getClientSettingsSnapshot();
}

export function useClientSettingsHydrated(): boolean {
return useSyncExternalStore(
subscribeClientSettingsHydration,
Expand Down
Loading
Loading