authorgravatar for 145564979+justsomelegs@users.noreply.github.comlegs <145564979+justsomelegs@users.noreply.github.com> 2026-04-23 18:59:08+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-23 13:59:08-04:00
log00b5c3e1b3b06888282e5d5e0678a2e3a88eb607
tree26b3ef0ebd708251b9c5cddd70fc2b39acbfd92b
parent188df6da074bf60af765881bdfd6e886ef83e6ee
signature Signed by PGP key B5690EEEBB952194

Add task sidebar auto-open setting (#2314)


5 files changed, 50 insertions(+), 4 deletions(-)

apps/desktop/src/clientPersistence.test.ts+1
......@@ -49,6 +49,7 @@ function makeSecretStorage(available: boolean): DesktopSecretStorage {
4949}
5050
5151const clientSettings: ClientSettings = {
52 autoOpenPlanSidebar: false,
5253 confirmThreadArchive: true,
5354 confirmThreadDelete: false,
5455 diffWordWrap: true,
apps/web/src/components/ChatView.tsx+15-4
......@@ -616,6 +616,7 @@ export default function ChatView(props: ChatViewProps) {
616616 (store) => store.setStickyModelSelection,
617617 );
618618 const timestampFormat = settings.timestampFormat;
619 const autoOpenPlanSidebar = settings.autoOpenPlanSidebar;
619620 const navigate = useNavigate();
620621 const rawSearch = useSearch({
621622 strict: false,
......@@ -2010,6 +2011,7 @@ export default function ChatView(props: ChatViewProps) {
20102011 planSidebarOpenOnNextThreadRef.current = false;
20112012 setPlanSidebarOpen(true);
20122013 } else {
2014 planSidebarOpenOnNextThreadRef.current = false;
20132015 setPlanSidebarOpen(false);
20142016 }
20152017 planSidebarDismissedForTurnRef.current = null;
......@@ -2018,6 +2020,7 @@ export default function ChatView(props: ChatViewProps) {
20182020 // Auto-open the plan sidebar when plan/todo steps arrive for the current turn.
20192021 // Don't auto-open for plans carried over from a previous turn (the user can open manually).
20202022 useEffect(() => {
2023 if (!autoOpenPlanSidebar) return;
20212024 if (!activePlan) return;
20222025 if (planSidebarOpen) return;
20232026 const latestTurnId = activeLatestTurn?.turnId ?? null;
......@@ -2025,7 +2028,13 @@ export default function ChatView(props: ChatViewProps) {
20252028 const turnKey = activePlan.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__";
20262029 if (planSidebarDismissedForTurnRef.current === turnKey) return;
20272030 setPlanSidebarOpen(true);
2028 }, [activePlan, activeLatestTurn?.turnId, planSidebarOpen, sidebarProposedPlan?.turnId]);
2031 }, [
2032 activePlan,
2033 activeLatestTurn?.turnId,
2034 autoOpenPlanSidebar,
2035 planSidebarOpen,
2036 sidebarProposedPlan?.turnId,
2037 ]);
20292038
20302039 useEffect(() => {
20312040 setIsRevertingCheckpoint(false);
......@@ -2950,7 +2959,7 @@ export default function ChatView(props: ChatViewProps) {
29502959 // Optimistically open the plan sidebar when implementing (not refining).
29512960 // "default" mode here means the agent is executing the plan, which produces
29522961 // step-tracking activities that the sidebar will display.
2953 if (nextInteractionMode === "default") {
2962 if (nextInteractionMode === "default" && autoOpenPlanSidebar) {
29542963 planSidebarDismissedForTurnRef.current = null;
29552964 setPlanSidebarOpen(true);
29562965 }
......@@ -2979,6 +2988,7 @@ export default function ChatView(props: ChatViewProps) {
29792988 runtimeMode,
29802989 setComposerDraftInteractionMode,
29812990 setThreadError,
2991 autoOpenPlanSidebar,
29822992 environmentId,
29832993 ],
29842994 );
......@@ -3071,8 +3081,8 @@ export default function ChatView(props: ChatViewProps) {
30713081 return waitForStartedServerThread(scopeThreadRef(activeThread.environmentId, nextThreadId));
30723082 })
30733083 .then(() => {
3074 // Signal that the plan sidebar should open on the new thread.
3075 planSidebarOpenOnNextThreadRef.current = true;
3084 // Signal that the plan sidebar should open on the new thread when enabled.
3085 planSidebarOpenOnNextThreadRef.current = autoOpenPlanSidebar;
30763086 return navigate({
30773087 to: "/$environmentId/$threadId",
30783088 params: {
......@@ -3113,6 +3123,7 @@ export default function ChatView(props: ChatViewProps) {
31133123 navigate,
31143124 resetLocalDispatch,
31153125 runtimeMode,
3126 autoOpenPlanSidebar,
31163127 environmentId,
31173128 ]);
31183129
apps/web/src/components/settings/SettingsPanels.tsx+30
......@@ -472,6 +472,9 @@ export function useSettingsRestore(onRestored?: () => void) {
472472 ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap
473473 ? ["Diff line wrapping"]
474474 : []),
475 ...(settings.autoOpenPlanSidebar !== DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar
476 ? ["Task sidebar"]
477 : []),
475478 ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming
476479 ? ["Assistant output"]
477480 : []),
......@@ -493,6 +496,7 @@ export function useSettingsRestore(onRestored?: () => void) {
493496 [
494497 areProviderSettingsDirty,
495498 isGitWritingModelDirty,
499 settings.autoOpenPlanSidebar,
496500 settings.confirmThreadArchive,
497501 settings.confirmThreadDelete,
498502 settings.addProjectBaseDirectory,
......@@ -945,6 +949,32 @@ export function GeneralSettingsPanel() {
945949 }
946950 />
947951
952 <SettingsRow
953 title="Task sidebar"
954 description="Open the plan and task sidebar automatically when steps appear."
955 resetAction={
956 settings.autoOpenPlanSidebar !== DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar ? (
957 <SettingResetButton
958 label="task sidebar"
959 onClick={() =>
960 updateSettings({
961 autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,
962 })
963 }
964 />
965 ) : null
966 }
967 control={
968 <Switch
969 checked={settings.autoOpenPlanSidebar}
970 onCheckedChange={(checked) =>
971 updateSettings({ autoOpenPlanSidebar: Boolean(checked) })
972 }
973 aria-label="Open the task sidebar automatically"
974 />
975 }
976 />
977
948978 <SettingsRow
949979 title="New threads"
950980 description="Pick the default workspace mode for newly created draft threads."
apps/web/src/localApi.test.ts+2
......@@ -529,6 +529,7 @@ describe("wsApi", () => {
529529
530530 it("reads and writes persistence through the desktop bridge when available", async () => {
531531 const clientSettings = {
532 autoOpenPlanSidebar: false,
532533 confirmThreadArchive: true,
533534 confirmThreadDelete: false,
534535 diffWordWrap: true,
......@@ -587,6 +588,7 @@ describe("wsApi", () => {
587588 const { createLocalApi } = await import("./localApi");
588589 const api = createLocalApi(rpcClientMock as never);
589590 const clientSettings = {
591 autoOpenPlanSidebar: false,
590592 confirmThreadArchive: true,
591593 confirmThreadDelete: false,
592594 diffWordWrap: true,
packages/contracts/src/settings.ts+2
......@@ -31,6 +31,7 @@ export type SidebarProjectGroupingMode = typeof SidebarProjectGroupingMode.Type;
3131export const DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE: SidebarProjectGroupingMode = "repository";
3232
3333export const ClientSettingsSchema = Schema.Struct({
34 autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
3435 confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
3536 confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
3637 diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
......@@ -243,6 +244,7 @@ export const ServerSettingsPatch = Schema.Struct({
243244export type ServerSettingsPatch = typeof ServerSettingsPatch.Type;
244245
245246export const ClientSettingsPatch = Schema.Struct({
247 autoOpenPlanSidebar: Schema.optionalKey(Schema.Boolean),
246248 confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
247249 confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
248250 diffWordWrap: Schema.optionalKey(Schema.Boolean),