Skip to content
Closed
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
12 changes: 6 additions & 6 deletions apps/web/src/components/ChatView.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ describe("ChatView timeline estimator parity (full app)", () => {

try {
const initialModeButton = await waitForInteractionModeButton("Chat");
expect(initialModeButton.title).toContain("enter plan mode");
expect(initialModeButton.getAttribute("title")).toBeNull();

window.dispatchEvent(
new KeyboardEvent("keydown", {
Expand All @@ -826,7 +826,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
);
await waitForLayout();

expect((await waitForInteractionModeButton("Chat")).title).toContain("enter plan mode");
expect(await waitForInteractionModeButton("Chat")).toBeTruthy();

const composerEditor = await waitForComposerEditor();
composerEditor.focus();
Expand All @@ -841,9 +841,8 @@ describe("ChatView timeline estimator parity (full app)", () => {

await vi.waitFor(
async () => {
expect((await waitForInteractionModeButton("Plan")).title).toContain(
"return to normal chat mode",
);
const planButton = await waitForInteractionModeButton("Plan");
expect(planButton.getAttribute("title")).toBeNull();
},
{ timeout: 8_000, interval: 16 },
);
Expand All @@ -859,7 +858,8 @@ describe("ChatView timeline estimator parity (full app)", () => {

await vi.waitFor(
async () => {
expect((await waitForInteractionModeButton("Chat")).title).toContain("enter plan mode");
const chatButton = await waitForInteractionModeButton("Chat");
expect(chatButton.getAttribute("title")).toBeNull();
},
{ timeout: 8_000, interval: 16 },
);
Expand Down
113 changes: 76 additions & 37 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
} from "@t3tools/shared/model";
import {
memo,
type ComponentProps,
type ReactNode,
useCallback,
useEffect,
useLayoutEffect,
Expand Down Expand Up @@ -251,6 +253,7 @@ function formatWorkingTimer(startIso: string, endIso: string): string | null {
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}


const LAST_EDITOR_KEY = "t3code:last-editor";
const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";
const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;
Expand Down Expand Up @@ -3720,49 +3723,85 @@ export default function ChatView({ threadId }: ChatViewProps) {
<Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />

{/* Interaction mode toggle */}
<Button
variant="ghost"
className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
size="sm"
type="button"
onClick={toggleInteractionMode}
title={
interactionMode === "plan"
? "Plan mode — click to return to normal chat mode"
: "Default mode — click to enter plan mode"
}
>
<BotIcon />
<span className="sr-only sm:not-sr-only">
{interactionMode === "plan" ? "Plan" : "Chat"}
</span>
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
size="sm"
type="button"
onClick={toggleInteractionMode}
aria-description={
interactionMode === "plan"
? "Plan mode — click to return to normal chat mode"
: "Default mode — click to enter plan mode"
}
>
<BotIcon />
<span className="sr-only sm:not-sr-only">
{interactionMode === "plan" ? "Plan" : "Chat"}
</span>
</Button>
}
/>
<TooltipPopup
side="top"
className="max-w-72 whitespace-normal leading-tight"
>
<span className="flex flex-col items-center gap-1">
<span>
{interactionMode === "plan"
? "Plan mode — click to return to normal chat mode"
: "Default mode — click to enter plan mode"}
</span>
<kbd className="inline-flex shrink-0 items-center gap-0.5 rounded bg-foreground/10 px-1 py-0.5 font-medium font-sans text-[10px]">
<span>⇧</span>
<span>Tab</span>
</kbd>
</span>
</TooltipPopup>
</Tooltip>

{/* Divider */}
<Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />

{/* Runtime mode toggle */}
<Button
variant="ghost"
className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
size="sm"
type="button"
onClick={() =>
void handleRuntimeModeChange(
runtimeMode === "full-access" ? "approval-required" : "full-access",
)
}
title={
runtimeMode === "full-access"
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"
size="sm"
type="button"
onClick={() =>
void handleRuntimeModeChange(
runtimeMode === "full-access" ? "approval-required" : "full-access",
)
}
aria-description={
runtimeMode === "full-access"
? "Full access — click to require approvals"
: "Approval required — click for full access"
}
>
{runtimeMode === "full-access" ? <LockOpenIcon /> : <LockIcon />}
<span className="sr-only sm:not-sr-only">
{runtimeMode === "full-access" ? "Full access" : "Supervised"}
</span>
</Button>
}
/>
<TooltipPopup
side="top"
className="max-w-72 whitespace-normal leading-tight"
>
{runtimeMode === "full-access"
? "Full access — click to require approvals"
: "Approval required — click for full access"
}
>
{runtimeMode === "full-access" ? <LockOpenIcon /> : <LockIcon />}
<span className="sr-only sm:not-sr-only">
{runtimeMode === "full-access" ? "Full access" : "Supervised"}
</span>
</Button>
: "Approval required — click for full access"}
</TooltipPopup>
</Tooltip>

{/* Plan sidebar toggle */}
{(activePlan || activeProposedPlan || planSidebarOpen) ? (
Expand Down
13 changes: 11 additions & 2 deletions apps/web/src/components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;

function TooltipTrigger(props: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
const { delay = 120, closeDelay = 80, ...restProps } = props;

return (
<TooltipPrimitive.Trigger
closeDelay={closeDelay}
data-slot="tooltip-trigger"
delay={delay}
{...restProps}
/>
);
}

function TooltipPopup({
Expand Down Expand Up @@ -38,7 +47,7 @@ function TooltipPopup({
>
<TooltipPrimitive.Popup
className={cn(
"relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) text-balance rounded-md border bg-popover not-dark:bg-clip-padding text-popover-foreground text-xs shadow-md/5 transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 data-instant:duration-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
"relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) text-balance rounded-md border border-border/70 bg-background/95 not-dark:bg-clip-padding text-[11px] font-medium text-foreground/90 shadow-lg/10 shadow-black/15 backdrop-blur-md transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 data-instant:duration-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
)}
data-slot="tooltip-popup"
Expand Down