| ... | @@ -130,6 +130,7 @@ import { buildThreadActionMenuItems } from "./threadActionMenu.logic"; | ... | @@ -130,6 +130,7 @@ import { buildThreadActionMenuItems } from "./threadActionMenu.logic"; |
| 130 | import { | 130 | import { |
| 131 | animatePinnedLayoutChanges, | 131 | animatePinnedLayoutChanges, |
| 132 | buildBulkTitleRegenerationContextMenuItem, | 132 | buildBulkTitleRegenerationContextMenuItem, |
| | 133 | buildBulkUnpinContextMenuItem, |
| 133 | filterSidebarProjectScopeItems, | 134 | filterSidebarProjectScopeItems, |
| 134 | formatWorkingDurationLabel, | 135 | formatWorkingDurationLabel, |
| 135 | firstValidTimestampMs, | 136 | firstValidTimestampMs, |
| ... | @@ -3025,10 +3026,22 @@ export default function Sidebar() { | ... | @@ -3025,10 +3026,22 @@ export default function Sidebar() { |
| 3025 | supportedCount: titleRegenerationThreads.length, | 3026 | supportedCount: titleRegenerationThreads.length, |
| 3026 | actionableCount: regeneratableTitleThreads.length, | 3027 | actionableCount: regeneratableTitleThreads.length, |
| 3027 | }); | 3028 | }); |
| | 3029 | // Unpin (k) counts only the pinned rows in pin-capable environments — |
| | 3030 | // on a mixed selection the unpinned rows are untouched, and the item |
| | 3031 | // is omitted entirely when nothing selected is pinned. |
| | 3032 | const pinnedSelectedThreads = selectedThreads.filter( |
| | 3033 | (thread) => |
| | 3034 | serverConfigs.get(thread.environmentId)?.environment.capabilities.threadPinning === |
| | 3035 | true && thread.pinnedAt != null, |
| | 3036 | ); |
| | 3037 | const unpinMenuItem = buildBulkUnpinContextMenuItem({ |
| | 3038 | pinnedCount: pinnedSelectedThreads.length, |
| | 3039 | }); |
| 3028 | const snoozePresets = resolveSnoozePresets(new Date(), timestampFormat); | 3040 | const snoozePresets = resolveSnoozePresets(new Date(), timestampFormat); |
| 3029 | const clicked = await settlePromise(() => | 3041 | const clicked = await settlePromise(() => |
| 3030 | api.contextMenu.show( | 3042 | api.contextMenu.show( |
| 3031 | [ | 3043 | [ |
| | 3044 | ...(unpinMenuItem ? [unpinMenuItem] : []), |
| 3032 | { id: "settle", label: `Settle (${count})` }, | 3045 | { id: "settle", label: `Settle (${count})` }, |
| 3033 | ...(canSnoozeSelection | 3046 | ...(canSnoozeSelection |
| 3034 | ? [ | 3047 | ? [ |
| ... | @@ -3110,6 +3123,14 @@ export default function Sidebar() { | ... | @@ -3110,6 +3123,14 @@ export default function Sidebar() { |
| 3110 | } | 3123 | } |
| 3111 | return; | 3124 | return; |
| 3112 | } | 3125 | } |
| | 3126 | if (clicked.value === "unpin") { |
| | 3127 | // Each unpin reports its own failure, like the single-row action. |
| | 3128 | for (const thread of pinnedSelectedThreads) { |
| | 3129 | attemptUnpin(scopeThreadRef(thread.environmentId, thread.id)); |
| | 3130 | } |
| | 3131 | clearSelection(); |
| | 3132 | return; |
| | 3133 | } |
| 3113 | if (clicked.value === "regenerate-title") { | 3134 | if (clicked.value === "regenerate-title") { |
| 3114 | for (const thread of regeneratableTitleThreads) { | 3135 | for (const thread of regeneratableTitleThreads) { |
| 3115 | const result = await updateThreadMetadata({ | 3136 | const result = await updateThreadMetadata({ |
| ... | @@ -3199,6 +3220,7 @@ export default function Sidebar() { | ... | @@ -3199,6 +3220,7 @@ export default function Sidebar() { |
| 3199 | [ | 3220 | [ |
| 3200 | attemptSettle, | 3221 | attemptSettle, |
| 3201 | attemptSnooze, | 3222 | attemptSnooze, |
| | 3223 | attemptUnpin, |
| 3202 | clearSelection, | 3224 | clearSelection, |
| 3203 | confirmThreadDelete, | 3225 | confirmThreadDelete, |
| 3204 | deleteThread, | 3226 | deleteThread, |