Skip to content
Open
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
11 changes: 8 additions & 3 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@
environmentId: EnvironmentId;
threadId: ThreadId;
onDiffPanelOpen?: () => void;
onOpenContextTab?: () => void;
reserveTitleBarControlInset?: boolean;
routeKind: "server";
draftId?: never;
Expand All @@ -348,6 +349,7 @@
environmentId: EnvironmentId;
threadId: ThreadId;
onDiffPanelOpen?: () => void;
onOpenContextTab?: () => void;
reserveTitleBarControlInset?: boolean;
routeKind: "draft";
draftId: DraftId;
Expand Down Expand Up @@ -1714,7 +1716,9 @@
replace: true,
search: (previous) => {
const rest = stripDiffSearchParams(previous);
return diffOpen ? { ...rest, diff: undefined } : { ...rest, diff: "1" };
return diffOpen
? { ...rest, diff: undefined, tab: undefined }
: { ...rest, diff: "1", tab: undefined };
},
});
}, [diffOpen, environmentId, isServerThread, navigate, onDiffPanelOpen, threadId]);
Expand Down Expand Up @@ -1780,7 +1784,7 @@
);

const focusComposer = useCallback(() => {
composerRef.current?.focusAtEnd();

Check warning on line 1787 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
}, []);
const scheduleComposerFocus = useCallback(() => {
window.requestAnimationFrame(() => {
Expand All @@ -1788,7 +1792,7 @@
});
}, [focusComposer]);
const addTerminalContextToDraft = useCallback((selection: TerminalContextSelection) => {
composerRef.current?.addTerminalContext(selection);

Check warning on line 1795 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
}, []);
const setTerminalOpen = useCallback(
(open: boolean) => {
Expand Down Expand Up @@ -2467,7 +2471,7 @@
const shortcutContext = {
terminalFocus: isTerminalFocused(),
terminalOpen: Boolean(terminalState.terminalOpen),
modelPickerOpen: composerRef.current?.isModelPickerOpen() ?? false,

Check warning on line 2474 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

react-hooks(exhaustive-deps)

React Hook useEffect has a missing dependency: 'composerRef.current'
};

const command = resolveShortcutCommand(event, keybindings, {
Expand Down Expand Up @@ -3019,7 +3023,7 @@
};
});
promptRef.current = "";
composerRef.current?.resetCursorState({ cursor: 0 });

Check warning on line 3026 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
},
[activePendingProgress?.activeQuestion, activePendingUserInput],
);
Expand All @@ -3046,7 +3050,7 @@
),
},
}));
const snapshot = composerRef.current?.readSnapshot();

Check warning on line 3053 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
if (
snapshot?.value !== value ||
snapshot.cursor !== nextCursor ||
Expand Down Expand Up @@ -3109,7 +3113,7 @@
return;
}

const sendCtx = composerRef.current?.getSendContext();

Check warning on line 3116 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
if (!sendCtx) {
return;
}
Expand Down Expand Up @@ -3246,7 +3250,7 @@
return;
}

const sendCtx = composerRef.current?.getSendContext();

Check warning on line 3253 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
if (!sendCtx) {
return;
}
Expand Down Expand Up @@ -3470,8 +3474,8 @@
search: (previous) => {
const rest = stripDiffSearchParams(previous);
return filePath
? { ...rest, diff: "1", diffTurnId: turnId, diffFilePath: filePath }
: { ...rest, diff: "1", diffTurnId: turnId };
? { ...rest, diff: "1", diffTurnId: turnId, diffFilePath: filePath, tab: undefined }
: { ...rest, diff: "1", diffTurnId: turnId, tab: undefined };
},
});
},
Expand Down Expand Up @@ -3676,6 +3680,7 @@
scheduleComposerFocus={scheduleComposerFocus}
setThreadError={setThreadError}
onExpandImage={onExpandTimelineImage}
{...(props.onOpenContextTab ? { onOpenContextTab: props.onOpenContextTab } : {})}
/>
</div>
</div>
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,16 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(
onPreviousPendingQuestion: () => void;
onInterrupt: () => void;
onImplementPlanInNewThread: () => void;
onOpenContextTab?: () => void;
}) {
return (
<>
{props.activeContextWindow ? <ContextWindowMeter usage={props.activeContextWindow} /> : null}
{props.activeContextWindow && props.onOpenContextTab ? (
<ContextWindowMeter
usage={props.activeContextWindow}
onOpenContextTab={props.onOpenContextTab}
/>
) : null}
{props.isPreparingWorktree ? (
<span className="text-muted-foreground/70 text-xs">Preparing worktree...</span>
) : null}
Expand Down Expand Up @@ -483,6 +489,7 @@ export interface ChatComposerProps {
scheduleComposerFocus: () => void;
setThreadError: (threadId: ThreadId | null, error: string | null) => void;
onExpandImage: (preview: ExpandedImagePreview) => void;
onOpenContextTab?: () => void;
}

// --------------------------------------------------------------------------
Expand Down Expand Up @@ -556,6 +563,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
scheduleComposerFocus,
setThreadError,
onExpandImage,
onOpenContextTab,
} = props;

// ------------------------------------------------------------------
Expand Down Expand Up @@ -2406,6 +2414,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion}
onInterrupt={handleInterruptPrimaryAction}
onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction}
{...(onOpenContextTab ? { onOpenContextTab } : {})}
/>
</div>
</div>
Expand Down
144 changes: 53 additions & 91 deletions apps/web/src/components/chat/ContextWindowMeter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { cn } from "~/lib/utils";
import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow";
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";

function formatPercentage(value: number | null): string | null {
if (value === null || !Number.isFinite(value)) {
Expand All @@ -12,103 +11,66 @@ function formatPercentage(value: number | null): string | null {
return `${Math.round(value)}%`;
}

export function ContextWindowMeter(props: { usage: ContextWindowSnapshot }) {
const { usage } = props;
export function ContextWindowMeter(props: {
usage: ContextWindowSnapshot;
onOpenContextTab: () => void;
}) {
const { usage, onOpenContextTab } = props;
const usedPercentage = formatPercentage(usage.usedPercentage);
const normalizedPercentage = Math.max(0, Math.min(100, usage.usedPercentage ?? 0));
const radius = 9.75;
const circumference = 2 * Math.PI * radius;
const dashOffset = circumference - (normalizedPercentage / 100) * circumference;

return (
<Popover>
<PopoverTrigger
openOnHover
delay={150}
closeDelay={0}
render={
<button
type="button"
className="group inline-flex items-center justify-center rounded-full transition-opacity hover:opacity-85"
aria-label={
usage.maxTokens !== null && usedPercentage
? `Context window ${usedPercentage} used`
: `Context window ${formatContextWindowTokens(usage.usedTokens)} tokens used`
}
>
<span className="relative flex h-6 w-6 items-center justify-center">
<svg
viewBox="0 0 24 24"
className="-rotate-90 absolute inset-0 h-full w-full transform-gpu"
aria-hidden="true"
>
<circle
cx="12"
cy="12"
r={radius}
fill="none"
stroke="color-mix(in oklab, var(--color-muted) 70%, transparent)"
strokeWidth="3"
/>
<circle
cx="12"
cy="12"
r={radius}
fill="none"
stroke="var(--color-muted-foreground)"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
className="transition-[stroke-dashoffset] duration-500 ease-out motion-reduce:transition-none"
/>
</svg>
<span
className={cn(
"relative flex h-[15px] w-[15px] items-center justify-center rounded-full bg-background text-[8px] font-medium",
"text-muted-foreground",
)}
>
{usage.usedPercentage !== null
? Math.round(usage.usedPercentage)
: formatContextWindowTokens(usage.usedTokens)}
</span>
</span>
</button>
}
/>
<PopoverPopup tooltipStyle side="top" align="end" className="w-max max-w-none px-3 py-2">
<div className="space-y-1.5 leading-tight">
<div className="text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
Context window
</div>
{usage.maxTokens !== null && usedPercentage ? (
<div className="whitespace-nowrap text-xs font-medium text-foreground">
<span>{usedPercentage}</span>
<span className="mx-1">⋅</span>
<span>{formatContextWindowTokens(usage.usedTokens)}</span>
<span>/</span>
<span>{formatContextWindowTokens(usage.maxTokens ?? null)} context used</span>
</div>
) : (
<div className="text-sm text-foreground">
{formatContextWindowTokens(usage.usedTokens)} tokens used so far
</div>
<button
type="button"
onClick={onOpenContextTab}
className="group/meter inline-flex cursor-pointer items-center justify-center rounded-full p-1 transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
aria-label={
usage.maxTokens !== null && usedPercentage
? `Context window ${usedPercentage} used`
: `Context window ${formatContextWindowTokens(usage.usedTokens)} tokens used`
}
>
<span className="relative flex h-6 w-6 items-center justify-center">
<svg
viewBox="0 0 24 24"
className="-rotate-90 absolute inset-0 h-full w-full transform-gpu"
aria-hidden="true"
>
<circle
cx="12"
cy="12"
r={radius}
fill="none"
stroke="color-mix(in oklab, var(--color-muted) 70%, transparent)"
strokeWidth="3"
/>
<circle
cx="12"
cy="12"
r={radius}
fill="none"
stroke="var(--color-muted-foreground)"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
className="transition-[stroke-dashoffset] duration-500 ease-out motion-reduce:transition-none"
/>
</svg>
<span
className={cn(
"relative flex h-3.75 w-3.75 items-center justify-center rounded-full bg-background text-[8px] font-medium transition-colors group-hover/meter:bg-accent",
"text-muted-foreground",
)}
{(usage.totalProcessedTokens ?? null) !== null &&
(usage.totalProcessedTokens ?? 0) > usage.usedTokens ? (
<div className="text-xs text-muted-foreground">
Total processed: {formatContextWindowTokens(usage.totalProcessedTokens ?? null)}{" "}
tokens
</div>
) : null}
{usage.compactsAutomatically ? (
<div className="text-xs text-muted-foreground">
Automatically compacts its context when needed.
</div>
) : null}
</div>
</PopoverPopup>
</Popover>
>
{usage.usedPercentage !== null
? Math.round(usage.usedPercentage)
: formatContextWindowTokens(usage.usedTokens)}
</span>
</span>
</button>
);
}
Loading
Loading