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
5 changes: 5 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"eslint": "^9.36.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-tailwindcss": "4.0.0-beta.0",
"jest": "^30.1.3",
"mermaid": "^11.12.0",
"playwright": "^1.56.0",
Expand Down Expand Up @@ -1517,6 +1518,8 @@

"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],

"eslint-plugin-tailwindcss": ["eslint-plugin-tailwindcss@4.0.0-beta.0", "", { "dependencies": { "fast-glob": "^3.2.5", "postcss": "^8.4.4", "synckit": "^0.11.4", "tailwind-api-utils": "^1.0.3" }, "peerDependencies": { "tailwindcss": "^3.4.0 || ^4.0.0" } }, "sha512-WWCajZgQu38Sd67ZCl2W6i3MRzqB0d+H8s4qV9iB6lBJbsDOIpIlj6R1Fj2FXkoWErbo05pZnZYbCGIU9o/DsA=="],

"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],

"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
Expand Down Expand Up @@ -2693,6 +2696,8 @@

"synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],

"tailwind-api-utils": ["tailwind-api-utils@1.0.3", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "local-pkg": "^1.1.1" }, "peerDependencies": { "tailwindcss": "^3.3.0 || ^4.0.0 || ^4.0.0-beta" } }, "sha512-KpzUHkH1ug1sq4394SLJX38ZtpeTiqQ1RVyFTTSY2XuHsNSTWUkRo108KmyyrMWdDbQrLYkSHaNKj/a3bmA4sQ=="],

"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],

"tailwindcss": ["tailwindcss@4.1.15", "", {}, "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ=="],
Expand Down
19 changes: 19 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import js from "@eslint/js";
import { defineConfig } from "eslint/config";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import tailwindcss from "eslint-plugin-tailwindcss";
import tseslint from "typescript-eslint";

/**
Expand Down Expand Up @@ -154,12 +155,21 @@ export default defineConfig([
plugins: {
react,
"react-hooks": reactHooks,
tailwindcss,
local: localPlugin,
},
settings: {
react: {
version: "detect",
},
tailwindcss: {
// Don't try to load Tailwind config (v4 doesn't export resolveConfig)
config: false,
// CSS files to check
cssFiles: ["**/*.css", "!**/node_modules", "!**/.*", "!**/dist", "!**/build"],
// Disable callees check to avoid resolving config
callees: [],
},
},
rules: {
...react.configs.recommended.rules,
Expand Down Expand Up @@ -240,6 +250,15 @@ export default defineConfig([
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",

// Tailwind CSS
"tailwindcss/classnames-order": "warn",
"tailwindcss/enforces-negative-arbitrary-values": "warn",
"tailwindcss/enforces-shorthand": "warn",
"tailwindcss/migration-from-tailwind-2": "warn",
"tailwindcss/no-arbitrary-value": "off",
"tailwindcss/no-contradicting-classname": "error",
"tailwindcss/no-custom-classname": "off",

// Safe Node.js patterns
"local/no-unsafe-child-process": "error",
"local/no-sync-fs-methods": "error",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"eslint": "^9.36.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-tailwindcss": "4.0.0-beta.0",
"jest": "^30.1.3",
"mermaid": "^11.12.0",
"playwright": "^1.56.0",
Expand Down
8 changes: 4 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ function AppInner() {

return (
<>
<div className="flex h-screen overflow-hidden bg-bg-dark [@media(max-width:768px)]:flex-col">
<div className="bg-bg-dark flex h-screen overflow-hidden [@media(max-width:768px)]:flex-col">
<LeftSidebar
projects={projects}
workspaceMetadata={workspaceMetadata}
Expand All @@ -682,8 +682,8 @@ function AppInner() {
sortedWorkspacesByProject={sortedWorkspacesByProject}
workspaceRecency={workspaceRecency}
/>
<div className="flex-1 flex flex-col overflow-hidden min-w-0 [@media(max-width:768px)]:w-full">
<div className="flex-1 flex overflow-hidden [@media(max-width:768px)]:flex-col">
<div className="flex min-w-0 flex-1 flex-col overflow-hidden [@media(max-width:768px)]:w-full">
<div className="flex flex-1 overflow-hidden [@media(max-width:768px)]:flex-col">
{selectedWorkspace ? (
<ErrorBoundary
workspaceInfo={`${selectedWorkspace.projectName}/${selectedWorkspace.namedWorkspacePath?.split("/").pop() ?? selectedWorkspace.workspaceId}`}
Expand All @@ -701,7 +701,7 @@ function AppInner() {
</ErrorBoundary>
) : (
<div
className="text-center max-w-3xl mx-auto w-full [&_h2]:text-white [&_h2]:mb-4 [&_h2]:font-bold [&_h2]:tracking-tight [&_p]:text-muted [&_p]:leading-[1.6]"
className="[&_p]:text-muted mx-auto w-full max-w-3xl text-center [&_h2]:mb-4 [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-white [&_p]:leading-[1.6]"
style={{
padding: "clamp(40px, 10vh, 100px) 20px",
fontSize: "clamp(14px, 2vw, 16px)",
Expand Down
28 changes: 14 additions & 14 deletions src/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
)}
style={{ containerType: "inline-size" }}
>
<div className="flex-1 flex flex-col items-center justify-center h-full text-placeholder text-center">
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center">
<h3 className="m-0 mb-2.5 text-base font-medium">Loading workspace...</h3>
</div>
</div>
Expand Down Expand Up @@ -291,7 +291,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
)}
style={{ containerType: "inline-size" }}
>
<div className="flex-1 flex flex-col items-center justify-center h-full text-placeholder text-center">
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center">
<h3 className="m-0 mb-2.5 text-base font-medium">Loading workspace...</h3>
</div>
</div>
Expand All @@ -307,7 +307,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
)}
style={{ containerType: "inline-size" }}
>
<div className="flex-1 flex flex-col items-center justify-center h-full text-placeholder text-center">
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center">
<h3 className="m-0 mb-2.5 text-base font-medium">No Workspace Selected</h3>
<p className="m-0 text-[13px]">
Select a workspace from the sidebar to view and interact with Claude
Expand All @@ -327,10 +327,10 @@ const AIViewInner: React.FC<AIViewProps> = ({
>
<div
ref={chatAreaRef}
className="flex-1 min-w-96 flex flex-col [@media(max-width:768px)]:min-w-0 [@media(max-width:768px)]:w-full [@media(max-width:768px)]:max-h-full"
className="flex min-w-96 flex-1 flex-col [@media(max-width:768px)]:max-h-full [@media(max-width:768px)]:w-full [@media(max-width:768px)]:min-w-0"
>
<div className="py-1 px-[15px] bg-separator border-b border-border-light flex justify-between items-center [@media(max-width:768px)]:py-2 [@media(max-width:768px)]:pl-[60px] [@media(max-width:768px)]:flex-wrap [@media(max-width:768px)]:gap-2">
<div className="font-semibold text-foreground flex items-center gap-2 min-w-0 overflow-hidden">
<div className="bg-separator border-border-light flex items-center justify-between border-b px-[15px] py-1 [@media(max-width:768px)]:flex-wrap [@media(max-width:768px)]:gap-2 [@media(max-width:768px)]:py-2 [@media(max-width:768px)]:pl-[60px]">
<div className="text-foreground flex min-w-0 items-center gap-2 overflow-hidden font-semibold">
<StatusIndicator
streaming={canInterrupt}
title={
Expand All @@ -342,16 +342,16 @@ const AIViewInner: React.FC<AIViewProps> = ({
workspaceId={workspaceId}
tooltipPosition="bottom"
/>
<span className="font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis min-w-0">
<span className="min-w-0 truncate font-mono text-xs">
{projectName} / {branch}
</span>
<span className="font-mono text-muted font-normal text-[11px] whitespace-nowrap overflow-hidden text-ellipsis min-w-0">
<span className="text-muted min-w-0 truncate font-mono text-[11px] font-normal">
{namedWorkspacePath}
</span>
<TooltipWrapper inline>
<button
onClick={handleOpenTerminal}
className="bg-transparent border-none cursor-pointer p-1 flex items-center justify-center text-muted transition-colors hover:text-foreground [&_svg]:w-4 [&_svg]:h-4"
className="text-muted hover:text-foreground flex cursor-pointer items-center justify-center border-none bg-transparent p-1 transition-colors [&_svg]:h-4 [&_svg]:w-4"
>
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75zm1.75-.25a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75zM7.25 8a.75.75 0 01-.22.53l-2.25 2.25a.75.75 0 01-1.06-1.06L5.44 8 3.72 6.28a.75.75 0 111.06-1.06l2.25 2.25c.141.14.22.331.22.53zm1.5 1.5a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z" />
Expand All @@ -364,7 +364,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
</div>
</div>

<div className="flex-1 relative overflow-hidden">
<div className="relative flex-1 overflow-hidden">
<div
ref={contentRef}
onWheel={markUserInteraction}
Expand All @@ -375,10 +375,10 @@ const AIViewInner: React.FC<AIViewProps> = ({
aria-busy={canInterrupt}
aria-label="Conversation transcript"
tabIndex={0}
className="h-full overflow-y-auto p-[15px] whitespace-pre-wrap break-words leading-[1.5]"
className="h-full overflow-y-auto p-[15px] leading-[1.5] break-words whitespace-pre-wrap"
>
{mergedMessages.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center h-full text-placeholder text-center [&_h3]:m-0 [&_h3]:mb-2.5 [&_h3]:text-base [&_h3]:font-medium [&_p]:m-0 [&_p]:text-[13px]">
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center [&_h3]:m-0 [&_h3]:mb-2.5 [&_h3]:text-base [&_h3]:font-medium [&_p]:m-0 [&_p]:text-[13px]">
<h3>No Messages Yet</h3>
<p>Send a message below to begin</p>
</div>
Expand All @@ -404,7 +404,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
</div>
{isAtCutoff && (
<div
className="my-5 py-3 px-[15px] text-xs font-medium text-center text-edit-mode bg-edit-mode/10"
className="text-edit-mode bg-edit-mode/10 my-5 px-[15px] py-3 text-center text-xs font-medium"
style={{
borderBottom: "3px solid",
borderImage:
Expand Down Expand Up @@ -463,7 +463,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
<button
onClick={jumpToBottom}
type="button"
className="absolute bottom-2 left-1/2 -translate-x-1/2 py-1 px-2 text-white border rounded-[20px] text-xs font-medium shadow-[0_4px_12px_rgba(0,0,0,0.3)] cursor-pointer transition-all duration-200 z-[100] font-primary backdrop-blur-[1px] hover:scale-105 active:scale-95"
className="font-primary absolute bottom-2 left-1/2 z-[100] -translate-x-1/2 cursor-pointer rounded-[20px] border px-2 py-1 text-xs font-medium text-white shadow-[0_4px_12px_rgba(0,0,0,0.3)] backdrop-blur-[1px] transition-all duration-200 hover:scale-105 active:scale-95"
style={{
background: "hsl(from var(--color-assistant-border) h s l / 0.1)",
borderColor: "hsl(from var(--color-assistant-border) h s l / 0.4)",
Expand Down
10 changes: 5 additions & 5 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({

return (
<div
className="relative pt-[5px] px-[15px] pb-[15px] bg-separator border-t border-border-light flex flex-col gap-1"
className="bg-separator border-border-light relative flex flex-col gap-1 border-t px-[15px] pt-[5px] pb-[15px]"
style={{ containerType: "inline-size" }}
data-component="ChatInputSection"
>
Expand All @@ -717,7 +717,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
ariaLabel="Slash command suggestions"
listId={commandListId}
/>
<div className="flex gap-2.5 items-end" data-component="ChatInputControls">
<div className="flex items-end gap-2.5" data-component="ChatInputControls">
<VimTextArea
ref={inputRef}
value={input}
Expand All @@ -742,13 +742,13 @@ export const ChatInput: React.FC<ChatInputProps> = ({
<ImageAttachments images={imageAttachments} onRemove={handleRemoveImage} />
<div className="flex flex-col gap-1" data-component="ChatModeToggles">
{editingMessage && (
<div className="text-[11px] text-edit-mode font-medium">
<div className="text-edit-mode text-[11px] font-medium">
Editing message ({formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel)
</div>
)}
<div className="flex items-center">
<ChatToggles modelString={preferredModel}>
<div className="flex items-center gap-1 mr-3 h-[11px] @[700px]:[&_.help-indicator-wrapper]:hidden">
<div className="mr-3 flex h-[11px] items-center gap-1 @[700px]:[&_.help-indicator-wrapper]:hidden">
<ModelSelector
ref={modelSelectorRef}
value={preferredModel}
Expand Down Expand Up @@ -779,7 +779,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
</span>
</div>
</ChatToggles>
<div className="flex items-center gap-1.5 ml-auto max-@[700px]:hidden">
<div className="max-@[700px]:hidden ml-auto flex items-center gap-1.5">
<div
className={cn(
"flex gap-0 bg-toggle-bg rounded",
Expand Down
20 changes: 10 additions & 10 deletions src/components/ChatInputToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface ChatInputToastProps {
}

export const SolutionLabel: React.FC<{ children: ReactNode }> = ({ children }) => (
<div className="text-muted-light text-[10px] mb-1 uppercase">{children}</div>
<div className="text-muted-light mb-1 text-[10px] uppercase">{children}</div>
);

export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss }) => {
Expand Down Expand Up @@ -61,27 +61,27 @@ export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss

if (isRichError) {
return (
<div className="absolute bottom-full left-[15px] right-[15px] mb-2 z-[1000] pointer-events-none [&>*]:pointer-events-auto">
<div className="pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 [&>*]:pointer-events-auto">
<div
role="alert"
aria-live="assertive"
className="bg-toast-fatal-bg border border-toast-fatal-border rounded px-3 py-2.5 text-xs text-danger-soft animate-[toastSlideIn_0.2s_ease-out] shadow-[0_4px_12px_rgba(0,0,0,0.3)]"
className="bg-toast-fatal-bg border-toast-fatal-border text-danger-soft animate-[toastSlideIn_0.2s_ease-out] rounded border px-3 py-2.5 text-xs shadow-[0_4px_12px_rgba(0,0,0,0.3)]"
>
<div className="flex items-start gap-1.5">
<span className="text-sm leading-none">âš </span>
<div className="flex-1">
{toast.title && <div className="font-semibold mb-1.5">{toast.title}</div>}
<div className="text-light leading-[1.4] mt-1.5">{toast.message}</div>
{toast.title && <div className="mb-1.5 font-semibold">{toast.title}</div>}
<div className="text-light mt-1.5 leading-[1.4]">{toast.message}</div>
{toast.solution && (
<div className="bg-dark rounded px-2 py-1.5 mt-2 font-monospace text-[11px] text-code-type">
<div className="bg-dark font-monospace text-code-type mt-2 rounded px-2 py-1.5 text-[11px]">
{toast.solution}
</div>
)}
</div>
<button
onClick={handleDismiss}
aria-label="Dismiss"
className="bg-transparent border-0 text-inherit cursor-pointer p-0 w-4 h-4 flex items-center justify-center text-base leading-none opacity-60 transition-opacity hover:opacity-100"
className="flex h-4 w-4 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-base leading-none text-inherit opacity-60 transition-opacity hover:opacity-100"
>
×
</button>
Expand All @@ -93,7 +93,7 @@ export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss

// Regular toast for simple messages and success
return (
<div className="absolute bottom-full left-[15px] right-[15px] mb-2 z-[1000] pointer-events-none [&>*]:pointer-events-auto">
<div className="pointer-events-none absolute right-[15px] bottom-full left-[15px] z-[1000] mb-2 [&>*]:pointer-events-auto">
<div
role={toast.type === "error" ? "alert" : "status"}
aria-live={toast.type === "error" ? "assertive" : "polite"}
Expand All @@ -107,14 +107,14 @@ export const ChatInputToast: React.FC<ChatInputToastProps> = ({ toast, onDismiss
>
<span className="text-sm leading-none">{toast.type === "success" ? "✓" : "⚠"}</span>
<div className="flex-1">
{toast.title && <div className="font-semibold mb-px text-[11px]">{toast.title}</div>}
{toast.title && <div className="mb-px text-[11px] font-semibold">{toast.title}</div>}
<div className="opacity-90">{toast.message}</div>
</div>
{toast.type === "error" && (
<button
onClick={handleDismiss}
aria-label="Dismiss"
className="bg-transparent border-0 text-inherit cursor-pointer p-0 w-4 h-4 flex items-center justify-center text-base leading-none opacity-60 transition-opacity hover:opacity-100"
className="flex h-4 w-4 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-base leading-none text-inherit opacity-60 transition-opacity hover:opacity-100"
>
×
</button>
Expand Down
Loading