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
7 changes: 3 additions & 4 deletions src/components/Messages/AssistantMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ModelDisplay } from "./ModelDisplay";
import { CompactingMessageContent } from "./CompactingMessageContent";
import { CompactionBackground } from "./CompactionBackground";
import type { KebabMenuItem } from "@/components/KebabMenu";
import { useClipboard } from "@/utils/ui/clipboard";

interface AssistantMessageProps {
message: DisplayedMessage & { type: "assistant" };
Expand All @@ -27,7 +28,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
clipboardWriteText = (data: string) => navigator.clipboard.writeText(data),
}) => {
const [showRaw, setShowRaw] = useState(false);
const [copied, setCopied] = useState(false);
const { copied, copy } = useClipboard({ writeText: clipboardWriteText });

const content = message.content;
const isStreaming = message.isStreaming;
Expand All @@ -44,9 +45,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({

const handleCopy = async () => {
try {
await clipboardWriteText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
await copy(content);
} catch (err) {
console.error("Failed to copy:", err);
}
Expand Down
9 changes: 4 additions & 5 deletions src/components/Messages/UserMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState } from "react";
import React from "react";
import type { DisplayedMessage } from "@/types/message";
import type { ButtonConfig } from "./MessageWindow";
import { MessageWindow } from "./MessageWindow";
import { TerminalOutput } from "./TerminalOutput";
import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
import type { KebabMenuItem } from "@/components/KebabMenu";
import { useClipboard } from "@/utils/ui/clipboard";

interface UserMessageProps {
message: DisplayedMessage & { type: "user" };
Expand All @@ -30,7 +31,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
isCompacting,
clipboardWriteText = defaultClipboardWriteText,
}) => {
const [copied, setCopied] = useState(false);
const { copied, copy } = useClipboard({ writeText: clipboardWriteText });

const content = message.content;

Expand All @@ -55,9 +56,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
);

try {
await clipboardWriteText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
await copy(content);
} catch (err) {
console.error("Failed to copy:", err);
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ThinkingSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const GLOW_INTENSITIES: Record<number, { track: string; thumb: string }> = {
const getTextStyle = (n: number) => {
if (n === 0) {
return {
color: "#606060",
color: "var(--color-subdued)",
fontWeight: 400,
textShadow: "none",
fontSize: "10px",
Expand Down
3 changes: 2 additions & 1 deletion src/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useRef, useLayoutEffect, createContext, useContext } from "react";
import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
import { TIMING } from "@/constants/timing";

// Context for passing hover state and trigger ref from wrapper to tooltip
interface TooltipContextValue {
Expand Down Expand Up @@ -39,7 +40,7 @@ export const TooltipWrapper: React.FC<TooltipWrapperProps> = ({ inline = false,
// Delay hiding to allow moving mouse to tooltip
leaveTimerRef.current = setTimeout(() => {
setIsHovered(false);
}, 100);
}, TIMING.TOOLTIP_LEAVE_DELAY);
};

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/tools/BashToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const BashToolCall: React.FC<BashToolCallProps> = ({
<span
className="ml-2 inline-block shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium whitespace-nowrap"
style={{
background: result.exitCode === 0 ? "#4caf50" : "#f44336",
background: result.exitCode === 0 ? "var(--color-success)" : "var(--color-error)",
color: "white",
}}
>
Expand Down
9 changes: 4 additions & 5 deletions src/components/tools/FileEditToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/to
import { TooltipWrapper, Tooltip } from "../Tooltip";
import { DiffContainer, DiffRenderer, SelectableDiffRenderer } from "../shared/DiffRenderer";
import { KebabMenu, type KebabMenuItem } from "../KebabMenu";
import { useClipboard } from "@/utils/ui/clipboard";

type FileEditOperationArgs =
| FileEditReplaceStringToolArgs
Expand Down Expand Up @@ -49,7 +50,7 @@ function renderDiff(
try {
const patches = parsePatch(diff);
if (patches.length === 0) {
return <div style={{ padding: "8px", color: "#888" }}>No changes</div>;
return <div style={{ padding: "8px", color: "var(--color-muted)" }}>No changes</div>;
}

// Render each hunk using SelectableDiffRenderer if we have a callback, otherwise DiffRenderer
Expand Down Expand Up @@ -99,16 +100,14 @@ export const FileEditToolCall: React.FC<FileEditToolCallProps> = ({
}) => {
const { expanded, toggleExpanded } = useToolExpansion(true);
const [showRaw, setShowRaw] = React.useState(false);
const [copied, setCopied] = React.useState(false);
const { copied, copy } = useClipboard();

const filePath = "file_path" in args ? args.file_path : undefined;

const handleCopyPatch = async () => {
if (result && result.success && result.diff) {
try {
await navigator.clipboard.writeText(result.diff);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
await copy(result.diff);
} catch (err) {
console.error("Failed to copy:", err);
}
Expand Down
9 changes: 4 additions & 5 deletions src/components/tools/ProposePlanToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
import { useStartHere } from "@/hooks/useStartHere";
import { TooltipWrapper, Tooltip } from "../Tooltip";
import { cn } from "@/lib/utils";
import { useClipboard } from "@/utils/ui/clipboard";

interface ProposePlanToolCallProps {
args: ProposePlanToolArgs;
Expand All @@ -30,7 +31,7 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
}) => {
const { expanded, toggleExpanded } = useToolExpansion(true); // Expand by default
const [showRaw, setShowRaw] = useState(false);
const [copied, setCopied] = useState(false);
const { copied, copy } = useClipboard();

// Format: Title as H1 + plan content for "Start Here" functionality
const startHereContent = `# ${args.title}\n\n${args.plan}`;
Expand All @@ -52,9 +53,7 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(args.plan);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
await copy(args.plan);
} catch (err) {
console.error("Failed to copy:", err);
}
Expand Down Expand Up @@ -159,7 +158,7 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
"px-2 py-1 text-[10px] font-mono rounded-sm cursor-pointer transition-all duration-150 active:translate-y-px hover:text-plan-mode"
)}
style={{
color: showRaw ? "var(--color-plan-mode)" : "#888",
color: showRaw ? "var(--color-plan-mode)" : "var(--color-muted)",
background: showRaw
? "color-mix(in srgb, var(--color-plan-mode), transparent 90%)"
: "transparent",
Expand Down
50 changes: 0 additions & 50 deletions src/components/ui/button.tsx

This file was deleted.

29 changes: 29 additions & 0 deletions src/constants/timing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Centralized timing constants for animations, transitions, and delays.
* All values are in milliseconds.
*/
export const TIMING = {
/** Duration to show "Copied!" feedback after clipboard operations */
COPY_FEEDBACK_DURATION: 2000,

/** Delay before hiding tooltip after mouse leaves trigger */
TOOLTIP_HIDE_DELAY: 300,

/** Delay when mouse leaves tooltip content area */
TOOLTIP_LEAVE_DELAY: 100,

/** Duration for modal animation transitions */
MODAL_ANIMATION_DELAY: 200,

/** Debounce delay for search inputs */
SEARCH_DEBOUNCE: 300,

/** Standard animation duration for UI transitions */
ANIMATION_STANDARD: 150,

/** Fast animation duration */
ANIMATION_FAST: 100,

/** Slow animation duration */
ANIMATION_SLOW: 300,
} as const;
1 change: 1 addition & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
--color-interrupted: hsl(38 92% 50%);
--color-review-accent: hsl(48 70% 50%);
--color-git-dirty: hsl(38 92% 50%);
--color-success: hsl(122 39% 49%); /* #4caf50 - success/pass */
--color-error: hsl(0 70% 50%);
--color-error-bg: hsl(0 32% 18%);

Expand Down
34 changes: 34 additions & 0 deletions src/utils/ui/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useState } from "react";
import { TIMING } from "@/constants/timing";

/**
* Hook for handling clipboard copy operations with temporary feedback.
*
* @param options - Configuration options
* @param options.duration - How long to show the "copied" state (milliseconds)
* @param options.writeText - Custom clipboard write function (for testing)
* @returns Object with `copied` state and `copy` function
*
* @example
* const { copied, copy } = useClipboard();
* <button onClick={() => copy(text)}>
* {copied ? "Copied!" : "Copy"}
* </button>
*/
export function useClipboard(options?: {
duration?: number;
writeText?: (text: string) => Promise<void>;
}) {
const duration = options?.duration ?? TIMING.COPY_FEEDBACK_DURATION;
const writeText = options?.writeText ?? ((text: string) => navigator.clipboard.writeText(text));

const [copied, setCopied] = useState(false);

const copy = async (text: string) => {
await writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), duration);
};

return { copied, copy };
}