diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts
index 9a51373cf..c684689fb 100644
--- a/apps/mobile/app.config.ts
+++ b/apps/mobile/app.config.ts
@@ -161,7 +161,7 @@ const config: ExpoConfig = {
slug: "t3-code",
platforms: ["ios", "android"],
scheme: variant.scheme,
- version: "1.0.1",
+ version: "1.0.2",
runtimeVersion: {
// Fingerprint (not appVersion) so an OTA only reaches binaries whose native
// project — native deps, config plugins, AND patches/ — matches the update.
diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx
index 590ca9cb5..81d883508 100644
--- a/apps/web/src/components/SidebarV2.tsx
+++ b/apps/web/src/components/SidebarV2.tsx
@@ -403,8 +403,12 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
snoozeSupported: boolean;
// Renders the pin glyph. Pinned cards keep the full settle/snooze quick
// actions: settling clears the pin server-side, and snoozing hides the
- // card until wake with the pin intact underneath. Pin/unpin themselves
- // live in the context menu only.
+ // card until wake with the pin intact underneath. The glyph is also the
+ // in-row pin state cue (the pinned block has no header), so it always
+ // shows while pinned; it only becomes a clickable unpin quick-action once
+ // the pinning capability is confirmed, and stays a passive marker while
+ // the descriptor is not loaded. Pinning itself lives in the context menu.
+ pinningSupported: boolean;
isPinned: boolean;
// Compact wake countdown ("2h") for rows in the snoozed shelf.
snoozeWakeLabelText: string | null;
@@ -432,6 +436,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
onUnsettle: (threadRef: ScopedThreadRef) => void;
onSnooze: (threadRef: ScopedThreadRef, preset: SnoozePreset) => void;
onUnsnooze: (threadRef: ScopedThreadRef) => void;
+ onUnpin: (threadRef: ScopedThreadRef) => void;
onAcknowledgeWoke: (threadRef: ScopedThreadRef, visitedAt: string) => void;
onChangeRequestState: (threadKey: string, state: "open" | "closed" | "merged" | null) => void;
}) {
@@ -450,6 +455,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
onThreadClick,
onUnsettle,
onUnsnooze,
+ onUnpin,
renamingTitle,
thread,
variant,
@@ -704,6 +710,14 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
},
[onUnsnooze, threadRef],
);
+ const handleUnpinClick = useCallback(
+ (event: ReactMouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ onUnpin(threadRef);
+ },
+ [onUnpin, threadRef],
+ );
const handleSnoozePreset = useCallback(
(preset: SnoozePreset) => {
onSnooze(threadRef, preset);
@@ -1001,11 +1015,23 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
)}
{props.isPinned ? (
-
+ props.pinningSupported ? (
+
+ ) : (
+
+ )
) : null}
{/* The visible state owns this slot's width: status at rest,
actions on hover/keyboard focus or while the popover is open. Keeping
@@ -2790,11 +2816,13 @@ export default function SidebarV2() {
openCommandPalette({ open: "new-thread-in" });
}, [isMobile, newThreadContext, projectGroups.length, setOpenMobile]);
- // Same resolution as v1: prefer the local-thread binding, fall back to
- // chat.new, no platform gating — web users have working shortcuts too.
+ // The button mirrors chat.new: in multi-project setups both route through
+ // the command palette's "New thread in..." picker, and in single-project
+ // setups both create immediately. chat.newLocal always creates directly, so
+ // it is only a correct label when chat.new is unbound.
const newThreadShortcutLabel =
- shortcutLabelForCommand(keybindings, "chat.newLocal") ??
- shortcutLabelForCommand(keybindings, "chat.new");
+ shortcutLabelForCommand(keybindings, "chat.new") ??
+ shortcutLabelForCommand(keybindings, "chat.newLocal");
return (
<>
@@ -3080,6 +3108,10 @@ export default function SidebarV2() {
serverConfigs.get(thread.environmentId)?.environment.capabilities
.threadSnooze === true
}
+ pinningSupported={
+ serverConfigs.get(thread.environmentId)?.environment.capabilities
+ .threadPinning === true
+ }
isPinned={section === "pinned"}
snoozeWakeLabelText={
section === "snoozed" && thread.snoozedUntil != null
@@ -3120,6 +3152,7 @@ export default function SidebarV2() {
onUnsettle={attemptUnsettle}
onSnooze={attemptSnooze}
onUnsnooze={attemptUnsnooze}
+ onUnpin={attemptUnpin}
onAcknowledgeWoke={acknowledgeWoke}
onChangeRequestState={handleChangeRequestState}
/>