Skip to content
Merged
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: 1 addition & 1 deletion apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
55 changes: 44 additions & 11 deletions apps/web/src/components/SidebarV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}) {
Expand All @@ -450,6 +455,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
onThreadClick,
onUnsettle,
onUnsnooze,
onUnpin,
renamingTitle,
thread,
variant,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1001,11 +1015,23 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
<span className="flex-1" />
)}
{props.isPinned ? (
<PinIcon
aria-label="Pinned"
role="img"
className="size-3 shrink-0 text-muted-foreground/65"
/>
props.pinningSupported ? (
<button
type="button"
aria-label="Unpin thread"
title="Unpin thread"
onClick={handleUnpinClick}
className="inline-flex cursor-pointer items-center rounded-sm text-muted-foreground/65 outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
>
<PinIcon aria-hidden className="size-3 shrink-0" />
</button>
) : (
<PinIcon
aria-label="Pinned"
role="img"
className="size-3 shrink-0 text-muted-foreground/65"
/>
)
) : null}
{/* The visible state owns this slot's width: status at rest,
actions on hover/keyboard focus or while the popover is open. Keeping
Expand Down Expand Up @@ -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 (
<>
<SidebarChromeHeader isElectron={isElectron} />
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3120,6 +3152,7 @@ export default function SidebarV2() {
onUnsettle={attemptUnsettle}
onSnooze={attemptSnooze}
onUnsnooze={attemptUnsnooze}
onUnpin={attemptUnpin}
onAcknowledgeWoke={acknowledgeWoke}
onChangeRequestState={handleChangeRequestState}
/>
Expand Down
Loading