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
10 changes: 6 additions & 4 deletions src/renderer/actions/panelActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ProjectLocation, Thread } from "@/shared/contracts";
import { readBridge } from "@/renderer/bridge";
import { useAppStore } from "@/renderer/state/appStore";
import { useDevTerminalStore } from "@/renderer/state/devTerminalStore";
import { hasDirtyEditorBuffers } from "@/renderer/state/fileEditorSelectors";
import { useFileEditorStore } from "@/renderer/state/fileEditorStore";
import { usePanelStore } from "@/renderer/state/panelStore";
import { useSharedSettings } from "@/renderer/state/sharedSettingsStore";
Expand Down Expand Up @@ -167,14 +168,15 @@ function applyFilesPanel(

const fileEditor = useFileEditorStore.getState();
const currentRoot = fileEditor.rootContext;
const hasDirtyBuffers = Object.values(fileEditor.buffers).some(
(buffer) => buffer.status === "ready" && buffer.isDirty,
);
const isSameContext =
currentRoot?.projectId === context.projectId &&
currentRoot?.worktreePath === context.worktreePath;

if (!isSameContext && hasDirtyBuffers && !window.confirm("Discard unsaved editor changes?")) {
if (
!isSameContext &&
hasDirtyEditorBuffers() &&
!window.confirm("Discard unsaved editor changes?")
) {
return;
}

Expand Down
22 changes: 22 additions & 0 deletions src/renderer/components/layout/UnifiedRightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
FolderOpen,
Gauge,
Globe,
Lock,
LockOpen,
Maximize2,
NotebookPen,
PanelRightClose,
Expand Down Expand Up @@ -60,6 +62,9 @@ export function UnifiedRightPanel(props: {
onOpenUsage?: () => void;
onOpenNotes?: () => void;
onOpenPorts?: () => void;
/** Whether the panel re-scopes itself to whichever thread is open. */
followsThread?: boolean;
onToggleFollowsThread?: () => void;
onClose: () => void;
}) {
const {
Expand Down Expand Up @@ -97,6 +102,8 @@ export function UnifiedRightPanel(props: {
onOpenUsage,
onOpenNotes,
onOpenPorts,
followsThread = false,
onToggleFollowsThread,
onClose,
} = props;
const { t } = useLingui();
Expand Down Expand Up @@ -258,6 +265,21 @@ export function UnifiedRightPanel(props: {
</button>
);
})}
{onToggleFollowsThread ? (
<button
type="button"
className={`${dragCtl} ${panelHeaderTabIconButtonClass(followsThread)}`}
title={
followsThread
? t`Unlock panel from the open thread`
: t`Lock panel to the open thread`
}
aria-pressed={followsThread}
onClick={onToggleFollowsThread}
>
{followsThread ? <Lock className="size-3.5" /> : <LockOpen className="size-3.5" />}
</button>
) : null}
<button
type="button"
className={`${dragCtl} ${panelHeaderIconButtonClass}`}
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/components/layout/sidebarChrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ export const panelHeaderRowClass =
export const panelHeaderTooltipTriggerResetClass =
"min-w-0 shrink-0 justify-start rounded border-0 bg-transparent p-0 shadow-none outline-none";

/**
* Floating chrome that hovers over a thread's conversation — the per-thread tool
* rail, the changes bubble above the composer, and the scroll-to-bottom button.
* One class so the three share a surface; the tint tracks `--sidebar-background`
* (see styles.css) so they read as app chrome in every theme.
*/
export const floatingChromeSurfaceClass =
"border border-border/15 bg-[var(--floating-chrome-surface)] shadow-lg backdrop-blur-md";

/** Icon button in panel headers (tab toggles, close, expand). */
export const panelHeaderIconButtonClass =
"inline-flex items-center justify-center rounded p-0.5 text-muted hover:text-foreground";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { Button } from "@heroui/react";
import { useLingui } from "@lingui/react/macro";
import { ArrowDown } from "lucide-react";
import { floatingChromeSurfaceClass } from "@/renderer/components/layout/sidebarChrome";
import { useAppStore } from "@/renderer/state/appStore";
import { isPanelResizing, subscribePanelResize } from "@/renderer/state/panelResizeSignal";
import {
Expand Down Expand Up @@ -745,7 +746,10 @@ export const ChatScrollControls = forwardRef<
size="sm"
aria-label={t`Scroll to bottom`}
onPress={handleScrollButtonPress}
className={`absolute bottom-4 right-4 z-10 transition-opacity duration-200 ease-out ${
/* Centered via a negative margin, not `-translate-x-1/2`: HeroUI's pressed
state animates `transform`, which would fight a translate and snap the
button sideways on click. */
className={`${floatingChromeSurfaceClass} absolute bottom-4 left-1/2 z-10 -ml-3.5 size-7 min-w-0 rounded-full transition-opacity duration-200 ease-out ${
showScrollDown ? "opacity-80 hover:opacity-100" : "pointer-events-none opacity-0"
}`}
>
Expand Down
45 changes: 45 additions & 0 deletions src/renderer/components/thread/ThreadChangesBubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useShallow } from "zustand/shallow";
import { useLingui } from "@lingui/react/macro";
import { showGitReviewPanel } from "@/renderer/actions/panelActions";
import { floatingChromeSurfaceClass } from "@/renderer/components/layout/sidebarChrome";
import { useGitStore } from "@/renderer/state/gitStore";

/**
* Translucent working-tree diff stat that floats over the top-right corner of
* the composer. Renders nothing when the thread's scope has no changes, and
* opens the docked Git review panel for that scope on click.
*/
export function ThreadChangesBubble(props: {
projectId: string;
worktreePath?: string | undefined;
}) {
const { t } = useLingui();
const { insertions, deletions } = useGitStore(
useShallow((s) => {
const status = props.worktreePath
? s.worktreeStatuses[props.worktreePath]
: s.statuses[props.projectId];
return {
insertions: status?.totalInsertions ?? 0,
deletions: status?.totalDeletions ?? 0,
};
}),
);

if (insertions === 0 && deletions === 0) return null;

return (
<button
type="button"
title={t`Review changes`}
aria-label={t`Review changes`}
/* Sized to a 28px pill — same height as the scroll-to-bottom circle and the
rail's icon buttons, so the floating chrome shares one scale. */
className={`${floatingChromeSurfaceClass} absolute bottom-full right-2 z-10 mb-1.5 flex h-7 items-center gap-1.5 rounded-full px-3 text-xs font-medium transition-colors hover:border-border/30`}
onClick={() => showGitReviewPanel(props.projectId, props.worktreePath)}
>
{insertions > 0 && <span className="text-success">+{insertions}</span>}
{deletions > 0 && <span className="text-danger">-{deletions}</span>}
</button>
);
}
7 changes: 6 additions & 1 deletion src/renderer/components/thread/ThreadComposerSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { useSharedSettings } from "@/renderer/state/sharedSettingsStore";
import { isDraftContentNonEmpty } from "@/renderer/state/slices/types";
import { selectActiveSubAgentParentItemIds } from "@/renderer/state/subAgentSelectors";
import { useThread } from "@/renderer/state/useThread";
import { ThreadChangesBubble } from "./ThreadChangesBubble";
import { ThreadComposer, type ComposerControl } from "./ThreadComposer";
import { supportsUsableFastMode } from "./threadDraftViewHelpers";
import { ThreadContextIndicator } from "./ThreadContextIndicator";
Expand Down Expand Up @@ -601,7 +602,11 @@ function ThreadComposerSectionInner(props: ThreadComposerSectionProps & { thread
return (
<>
{thread.status !== "launching" || !usesTerminalPresentation ? (
<div>
<div className="relative">
<ThreadChangesBubble
projectId={thread.projectId}
{...(thread.worktreePath ? { worktreePath: thread.worktreePath } : {})}
/>
<div
className={`grid transition-[grid-template-rows] ease-[cubic-bezier(0.16,1,0.3,1)] ${isComposerCollapsed ? "duration-300" : "duration-200"}`}
style={{ gridTemplateRows: isComposerCollapsed ? "0fr" : "1fr" }}
Expand Down
Loading