diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 1c17d58215ea..0ee5bca8864d 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -20,6 +20,7 @@ const clientSettings: ClientSettings = { confirmQuit: true, confirmThreadArchive: true, confirmThreadDelete: false, + confirmThreadUnpin: false, dismissedProviderUpdateNotificationKeys: [], diffIgnoreWhitespace: true, environmentIdentificationMode: "artwork", diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index af7cdc8a94a2..08512864d377 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1718,6 +1718,7 @@ export default function Sidebar() { const autoSettleOnMerge = useClientSettings((s) => s.sidebarAutoSettleOnMerge); const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); + const confirmThreadUnpin = useClientSettings((s) => s.confirmThreadUnpin); const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder); const timestampFormat = useClientSettings((s) => s.timestampFormat); const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); @@ -2647,6 +2648,21 @@ export default function Sidebar() { const attemptUnpin = useCallback( (threadRef: ScopedThreadRef) => { void (async () => { + if (confirmThreadUnpin) { + const api = readLocalApi(); + const thread = threadByKeyRef.current.get(scopedThreadKey(threadRef)); + if (api) { + const confirmed = await settlePromise(() => + api.dialogs.confirm( + [ + `Unpin thread "${thread?.title ?? "this thread"}"?`, + "This will move the thread out of your pinned section.", + ].join("\n"), + ), + ); + if (confirmed._tag === "Failure" || !confirmed.value) return; + } + } const result = await unpinThread(threadRef); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); @@ -2660,7 +2676,7 @@ export default function Sidebar() { } })(); }, - [unpinThread], + [confirmThreadUnpin, unpinThread], ); const handlePinnedDragEnd = useCallback( diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 9539f95914cb..154782fc0e3a 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -527,6 +527,9 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete ? ["Delete confirmation"] : []), + ...(settings.confirmThreadUnpin !== DEFAULT_UNIFIED_SETTINGS.confirmThreadUnpin + ? ["Unpin confirmation"] + : []), ...(settings.confirmQuit !== DEFAULT_UNIFIED_SETTINGS.confirmQuit ? ["Quit confirmation"] : []), @@ -547,6 +550,7 @@ export function useSettingsRestore(onRestored?: () => void) { settings.confirmQuit, settings.confirmThreadArchive, settings.confirmThreadDelete, + settings.confirmThreadUnpin, settings.addProjectBaseDirectory, settings.defaultThreadEnvMode, settings.newWorktreesStartFromOrigin, @@ -658,6 +662,7 @@ export function useSettingsRestore(onRestored?: () => void) { addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory, confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive, confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete, + confirmThreadUnpin: DEFAULT_UNIFIED_SETTINGS.confirmThreadUnpin, confirmQuit: DEFAULT_UNIFIED_SETTINGS.confirmQuit, textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection, fontFamilySans: DEFAULT_UNIFIED_SETTINGS.fontFamilySans, @@ -2227,6 +2232,32 @@ export function GeneralSettingsPanel() { } /> + + updateSettings({ + confirmThreadUnpin: DEFAULT_UNIFIED_SETTINGS.confirmThreadUnpin, + }) + } + /> + ) : null + } + control={ + + updateSettings({ confirmThreadUnpin: Boolean(checked) }) + } + aria-label="Confirm thread unpinning" + /> + } + /> + s.sidebarAutoSettleOnMerge); const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); + const confirmThreadUnpin = useClientSettings((s) => s.confirmThreadUnpin); const timestampFormat = useClientSettings((s) => s.timestampFormat); const { copyToClipboard: copyPathToClipboard } = useCopyToClipboard<{ path: string }>({ onCopy: ({ path }) => { @@ -215,9 +216,21 @@ export function useThreadActionMenu(input: { case "pin": await reportFailure("Failed to pin thread", () => pinThread(threadRef)); return; - case "unpin": + case "unpin": { + if (confirmThreadUnpin) { + const confirmed = await settlePromise(() => + api.dialogs.confirm( + [ + `Unpin thread "${thread.title}"?`, + "This will move the thread out of your pinned section.", + ].join("\n"), + ), + ); + if (confirmed._tag === "Failure" || !confirmed.value) return; + } await reportFailure("Failed to unpin thread", () => unpinThread(threadRef)); return; + } case "rename": onStartRename(); return; @@ -315,6 +328,7 @@ export function useThreadActionMenu(input: { changeRequest, confirmThreadArchive, confirmThreadDelete, + confirmThreadUnpin, copyBranchToClipboard, copyPathToClipboard, copyThreadIdToClipboard, diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 0502d303d249..21841492b13c 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -156,6 +156,7 @@ export const ClientSettingsSchema = Schema.Struct({ confirmQuit: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), + confirmThreadUnpin: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe( Schema.withDecodingDefault(Effect.succeed([])), ), @@ -867,6 +868,7 @@ export const ClientSettingsPatch = Schema.Struct({ confirmQuit: Schema.optionalKey(Schema.Boolean), confirmThreadArchive: Schema.optionalKey(Schema.Boolean), confirmThreadDelete: Schema.optionalKey(Schema.Boolean), + confirmThreadUnpin: Schema.optionalKey(Schema.Boolean), diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode), glassOpacity: Schema.optionalKey(GlassOpacity),