Conversation
- migrate web UI to shared Base UI primitives (alerts, buttons, combobox, toast, etc.) - refactor chat and branch toolbars to use new components and cleaner interactions - add cross-platform file-manager fallback in `openInEditor` server handling - update model contracts/tests and web dependencies to support new UI flow
- Change primary Git action button from ghost to outline - Update Git action options trigger button to outline for consistent affordance
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a server-side git pull API and WS method; enriches git status with workingTree stats; updates contracts/NativeApi; adds React Query helpers; implements comprehensive UI primitives, git UI logic, toast system, and related tests. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Web Client
participant WS as WS Server
participant Service as GitCoreService
participant Repo as Git Repository
Client->>WS: WS_METHODS.gitPull { cwd }
WS->>Service: pullCurrentBranch(cwd)
Service->>Repo: ensure not detached HEAD & upstream exists
Service->>Repo: get HEAD SHA (before)
Service->>Repo: git pull --ff-only
Service->>Repo: get HEAD SHA (after)
Service->>Service: compare SHAs => status ("pulled" / "skipped_up_to_date")
Service->>Repo: get upstream branch info (optional)
Service-->>WS: GitPullResult { status, branch, upstreamBranch? }
WS-->>Client: return result or error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
Add Git pull API and working tree stats to server and wire new Git actions UI with toast-driven flows in web to tidy up git actionsIntroduce server-side 📍Where to StartStart with Macroscope summarized 0d4a38f. |
Greptile SummaryRefactored git actions UI from custom modal-based workflow to Base UI components with toast notifications. Added git pull functionality with proper upstream validation. Extracted business logic into testable pure functions with comprehensive unit tests. Migrated multiple components to use Base UI library ( Key improvements:
Confidence Score: 5/5
Important Files Changed
Flowchartflowchart TD
A[User clicks Git Action button] --> B{Quick Action Type}
B -->|run_action| C[Execute Git Action]
B -->|run_pull| D[Execute Git Pull]
B -->|open_pr| E[Open PR in Browser]
B -->|show_hint| F[Show Toast Hint]
C --> G{Is Default Branch?}
G -->|Yes| H[Show Confirmation Dialog]
G -->|No| I[Execute Action]
H -->|Confirmed| I
H -->|Cancelled| J[Cancel]
I --> K{Action Type}
K -->|commit| L[Generate/Use Commit Message]
K -->|commit_push| M[Commit + Push]
K -->|commit_push_pr| N[Commit + Push + Create PR]
L --> O[Show Toast with Result]
M --> O
N --> O
D --> O
E --> P[External Browser]
F --> Q[Display Hint]
O --> R[Refresh Git Status]
R --> S[Update UI State]
Last reviewed commit: 5aaae6c |
There was a problem hiding this comment.
Actionable comments posted: 15
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/web/src/index.css (1)
46-52:⚠️ Potential issue | 🟠 MajorEnable Tailwind directives in Biome to avoid CSS parse failures.
Biome reports Tailwind-specific syntax is disabled; this file uses
@apply, which can fail lint/CI. Please enablecss.parser.tailwindDirectives(or equivalent) in Biome config if Tailwind CSS is expected here.packages/contracts/src/model.ts (1)
19-30:⚠️ Potential issue | 🟡 MinorType assertion bypasses validation, return type may be misleading.
Line 29 casts
trimmed as ModelSlugwithout verifying thattrimmedis actually a valid slug fromMODEL_OPTIONS. WhileresolveModelSlugvalidates downstream, callers ofnormalizeModelSlugmay incorrectly assume the return value is always a validModelSlug.Consider either:
- Renaming to indicate it doesn't guarantee validity (e.g.,
normalizeModelInput)- Changing return type to
string | null- Adding validation here as well
Option: Return string | null to reflect actual behavior
-export function normalizeModelSlug(model: string | null | undefined): ModelSlug | null { +export function normalizeModelSlug(model: string | null | undefined): string | null { if (typeof model !== "string") { return null; } const trimmed = model.trim(); if (!trimmed) { return null; } - return MODEL_SLUG_ALIASES[trimmed] ?? (trimmed as ModelSlug); + return MODEL_SLUG_ALIASES[trimmed] ?? trimmed; }
🤖 Fix all issues with AI agents
In `@apps/web/src/App.tsx`:
- Line 13: The import in App.tsx currently pulls AnchoredToastProvider and
ToastProvider from a local path; update the code to use the centralized UI
package by importing AnchoredToastProvider and ToastProvider from "packages/ui"
(or the published package name used in the monorepo), and if they are not yet
re-exported from packages/ui, add re-exports there (export {
AnchoredToastProvider, ToastProvider } from "apps/web/src/components/ui/toast"
or from their original module) so all apps import these providers from the
shared packages/ui entrypoint.
In `@apps/web/src/components/ChatView.tsx`:
- Around line 51-68: Replace local UI imports referencing "./ui/..." with
imports from the shared package "packages/ui" for the UI primitives used in
ChatView: Alert, AlertAction, AlertDescription, AlertTitle, Button, Select,
SelectItem, SelectPopup, SelectTrigger, SelectValue, Separator, Group,
GroupSeparator, Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger, and Badge;
keep local imports that are not part of the design system (e.g., CursorIcon/Icon
from "./Icons") unchanged. Update the import statements so they import those
named symbols from "packages/ui" instead of "./ui/*" to align with the shared
design system.
- Around line 1437-1441: activeProject is being looked up by comparing
project.id to state.activeThreadId which is incorrect; update the lookup to use
the active thread's project id (or state.activeProjectId if available).
Specifically, use the existing activeThread (ensure it is defined) and resolve
activeProject via state.projects.find(p => p.id === activeThread.projectId), and
add a guard so OpenInPicker falls back or aborts when activeThread or
activeProject is undefined.
In `@apps/web/src/components/ui/dialog.tsx`:
- Around line 5-7: Replace local UI imports so Button and ScrollArea are
consumed from the shared package instead of internal paths: keep the cn import
from "~/lib/utils" but change the imports for Button and ScrollArea (referencing
the Button and ScrollArea symbols) to come from "packages/ui" per the shared UI
guideline; update any other occurrences in this module that import those symbols
from "~/components/ui/*" to use the package export instead.
- Around line 3-8: The file is using React.ComponentProps for typing (e.g.,
types derived from DialogPrimitive) but React isn’t imported, causing TS errors;
add a type-only import such as import type { ComponentProps } from "react" (or
import * as React from "react") at the top of the file so usages like
ComponentProps<typeof DialogPrimitive> compile, and update any existing
React.ComponentProps references to just ComponentProps if you use the type-only
import.
In `@apps/web/src/components/ui/empty.tsx`:
- Around line 1-4: This file uses React.ComponentProps but never imports
React—add an import for React (e.g., import * as React from "react") so all
React types resolve; also fix the semantic mismatch in the EmptyDescription
component by aligning its prop type with the rendered element (either change
EmptyDescription's React.ComponentProps<"p"> to React.ComponentProps<"div"> if
you intend to render a div, or change the rendered <div> to a <p> to keep the
"p" prop type) and update any other components using React.ComponentProps
accordingly (check usages in EmptyTitle/EmptyDescription and any functions
referenced by those names).
- Around line 46-79: The inner presentational div in the EmptyMedia component is
currently spreading {...props} and causing duplicate event handlers/IDs; remove
the {...props} spread from the inner div that renders
className={cn(emptyMediaVariants({ className, variant }))} so that props remain
only on the outer wrapper div (the first <div> with className={cn("relative
mb-6", className)} and data-slot/data-variant). Keep all other behavior (variant
handling, emptyMediaVariants, cn, and the conditional icon blocks) unchanged.
In `@apps/web/src/components/ui/fieldset.tsx`:
- Around line 1-29: The Fieldset wrapper and FieldsetLegend (functions Fieldset
and FieldsetLegend) are currently defined in the app and should be moved into
the shared UI package; extract these components from their current file and add
them to the packages/ui package (re-exporting the primitives from
"@base-ui/react/fieldset" as done now), then update all usages in apps/web to
import Fieldset and FieldsetLegend from the packages/ui package instead of the
local file; ensure the new module preserves the same props types
(FieldsetPrimitive.Root.Props and FieldsetPrimitive.Legend.Props), className
handling via cn, data-slot attributes, and update any barrel exports in
packages/ui so consumers can import the components from the package.
In `@apps/web/src/components/ui/form.tsx`:
- Around line 1-17: The Form primitive defined as the React component Form
(using FormPrimitive from "@base-ui/react/form" and cn from "~/lib/utils") must
be moved into the shared UI package and imported from there; create an
equivalent export in packages/ui (e.g., export a Form component that wraps
FormPrimitive with the same props and className logic), update all usages in the
app to import Form from the packages/ui package instead of the local component,
and remove the local apps/web Form component to avoid duplicate primitives;
ensure the moved component preserves the signature Form({ className, ...props }:
FormPrimitive.Props) and re-exports exactly the same API.
In `@apps/web/src/components/ui/group.tsx`:
- Around line 8-9: Replace the app-local Separator import in
components/ui/group.tsx (currently "import { Separator } from \"./separator\"")
with the shared package import from the mono-repo UI package (e.g., "import {
Separator } from 'ui'") so the component uses the centralized Separator from
packages/ui; update the import statement accordingly and remove any reliance on
the local ./separator file in this module (leave cn import unchanged).
In `@apps/web/src/components/ui/input-group.tsx`:
- Around line 6-8: The InputGroup component currently imports Input, InputProps,
Textarea, and TextareaProps from local paths (and cn from utils); update the
imports to use the shared UI package instead by replacing the local imports of
Input, InputProps, Textarea, and TextareaProps with imports from "packages/ui"
(or move this component into the shared package and re-export), ensuring you
still use the existing symbols cn, Input, InputProps, Textarea, and
TextareaProps where referenced in the component.
In `@apps/web/src/components/ui/sheet.tsx`:
- Around line 5-7: Update the imports in components/ui/sheet.tsx to consume
shared UI components from the packages/ui package: keep cn from "~/lib/utils"
but change the Button and ScrollArea imports (referenced as Button and
ScrollArea in this file) to import from "packages/ui" instead of
"~/components/ui/button" and "~/components/ui/scroll-area"; adjust any named
import syntax accordingly so the rest of the file (e.g., usage of Button and
ScrollArea) remains unchanged.
- Around line 3-8: Add the missing React type import so usages of
React.ComponentProps<"div"> compile: import React (or import * as React) at the
top of the file and ensure the file now references React for the ComponentProps
types used in the SheetPrimitive-related components (where
React.ComponentProps<"div"> appears around the
SheetRoot/SheetContent/SheetFooter definitions). No other code changes
required—just add the React import alongside the existing imports (e.g., with
the imports for SheetPrimitive, XIcon, cn, Button, ScrollArea) so TypeScript
recognizes React.ComponentProps.
In `@apps/web/src/components/ui/toast.tsx`:
- Around line 152-158: The Toast.Action currently renders only
toast.actionProps.children and drops actionable props (like onClick,
aria-attributes); update the Toast.Action usage to spread toast.actionProps onto
the component (e.g., <Toast.Action {...toast.actionProps} />) and merge/compose
the className returned from buttonVariants({ size: "xs" }) with any className
present on toast.actionProps so passed handlers and attributes are preserved;
apply the same change for the other instance that uses toast.actionProps (the
block referenced at lines 244–250).
In `@apps/web/src/lib/utils.ts`:
- Around line 1-6: Add the missing clsx dependency to the web app so the import
in utils.ts (import { type ClassValue, clsx } from "clsx"; and the cn(...inputs:
ClassValue[]) function) resolves at runtime: update apps/web/package.json
dependencies to include a compatible "clsx" entry (match the project’s package
manager style and versioning) and run install to ensure clsx is available for
the cn function and any other imports.
🟡 Minor comments (6)
apps/web/vite.config.ts-20-25 (1)
20-25:⚠️ Potential issue | 🟡 MinorUse
experimental.enableNativePlugin: 'v1'instead oftrue.
resolve.tsconfigPaths: trueis correct and valid for Vite 8. However,experimental.enableNativePluginshould be set to the string value'v1'rather than the booleantrue. In Vite 8, the documented default for native plugin support is'v1'. While Vite ignores unknown config values silently, using the correct syntax ensures the native plugin integration works as intended, especially since'resolver'was removed in Vite 8.0.0-beta.11.apps/web/src/components/ui/scroll-area.tsx-22-33 (1)
22-33:⚠️ Potential issue | 🟡 MinorChange
transition-shadowstotransition-shadowon line 24.The utility should use singular form
transition-shadowto match standard Tailwind syntax and align with all other UI components in the codebase (textarea, radio-group, input, group, etc.).apps/web/src/components/ChatView.tsx-1469-1474 (1)
1469-1474:⚠️ Potential issue | 🟡 MinorUpdate the aria-label to match the “Open in editor” action group.
💡 Suggested fix
- <Group aria-label="Subscription actions"> + <Group aria-label="Open in editor actions">apps/web/src/components/ui/select.tsx-176-186 (1)
176-186:⚠️ Potential issue | 🟡 MinorFix the SVG xmlns to use the standard SVG namespace.
The namespace URL contains an invalid year (1500 instead of 2000) and can cause rendering issues in some environments.
Suggested fix
- xmlns="http://www.w3.org/1500/svg" + xmlns="http://www.w3.org/2000/svg"apps/web/src/components/ui/empty.tsx-94-103 (1)
94-103:⚠️ Potential issue | 🟡 MinorChange EmptyDescription to render
<p>to match its declared props type.The component declares
React.ComponentProps<"p">but renders a<div>, creating a semantic and type mismatch. This is inconsistent with the other components in the file (Empty, EmptyHeader, EmptyTitle, EmptyContent, EmptyMedia) which all render the element type matching their declared props.Suggested fix
- <div + <p className={cn( "text-muted-foreground text-sm [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4 [[data-slot=empty-title]+&]:mt-1", className, )} data-slot="empty-description" {...props} />apps/web/src/components/ui/alert-dialog.tsx-82-96 (1)
82-96:⚠️ Potential issue | 🟡 MinorMissing explicit React import for type annotations.
AlertDialogHeaderandAlertDialogFooteruseReact.ComponentProps<"div">but React is not imported. While React 19 supports automatic JSX transform, explicit type usage likeReact.ComponentPropsrequires an import.🔧 Proposed fix
"use client"; import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; +import type * as React from "react"; import { cn } from "~/lib/utils";
🧹 Nitpick comments (9)
apps/web/src/model-logic.ts (1)
8-10: Consider defining REASONING_OPTIONS in contracts with a Zod schema for consistency.
REASONING_OPTIONSandReasoningEffortalign with the pattern used for model-related exports (DEFAULT_MODEL,MODEL_OPTIONS) from@t3tools/contracts. Theeffortfield is included in the sharedproviderSendTurnInputSchema(packages/contracts/src/provider.ts:88) for provider API payloads, but valid effort values are only defined in the web app. DefiningREASONING_OPTIONSas an enum in contracts with Zod validation would improve consistency and provide runtime validation for the shared protocol, as per coding guidelines on using Zod schemas for shared type contracts.packages/contracts/src/git.test.ts (1)
90-98: Consider adding coverage for theskipped_up_to_datestatus variant.The test covers the
"pulled"status, butgitPullResultSchemaalso accepts"skipped_up_to_date". Adding a test case for this variant would ensure complete schema coverage.📝 Suggested additional test case
it("parses git pull result", () => { const parsed = gitPullResultSchema.parse({ status: "pulled", branch: "feature/my-branch", upstreamBranch: "origin/feature/my-branch", }); expect(parsed.status).toBe("pulled"); expect(parsed.branch).toBe("feature/my-branch"); }); + + it("parses git pull result when already up to date", () => { + const parsed = gitPullResultSchema.parse({ + status: "skipped_up_to_date", + branch: "main", + }); + expect(parsed.status).toBe("skipped_up_to_date"); + });apps/web/src/components/ui/textarea.tsx (1)
9-12: Consider documenting the purpose ofnumberin the size type.The
sizeprop accepts"sm" | "default" | "lg" | number, but the number value only flows todata-sizeattribute and doesn't affect styling (lines 37-40 only check for"sm"and"lg"strings). If this is intentional for custom data attributes or future extensibility, a brief comment would clarify the design.apps/web/src/components/ChatView.tsx (1)
889-918: Avoiddisabledon Buttons here; use aria-disabled + click guards instead.
This preserves accessibility and tooltip behavior while still preventing double-submit. Please apply this pattern to all four approval buttons.💡 Example pattern (apply to each action)
- <Button - size="xs" - variant="default" - disabled={isResponding} - onClick={() => void onRespondToApproval(approval.requestId, "accept")} - > + <Button + size="xs" + variant="default" + aria-disabled={isResponding} + className={cn(isResponding && "pointer-events-none opacity-60")} + onClick={() => { + if (isResponding) return; + void onRespondToApproval(approval.requestId, "accept"); + }} + >Based on learnings: "Avoid using disabled props on buttons as they harm accessibility and break tooltips. Instead, use styling (e.g., opacity, hover states) and handle the disabled state through click handlers."
apps/web/src/components/ui/menu.tsx (1)
136-150: Consider using lucide-react CheckIcon for consistency.The MenuCheckboxItem and MenuRadioItem components use inline SVG for the checkmark indicator, while the file already imports
ChevronRightIconfrom lucide-react. UsingCheckIconfrom lucide-react would maintain icon consistency across the component library.♻️ Optional refactor for consistency
-import { ChevronRightIcon } from "lucide-react"; +import { CheckIcon, ChevronRightIcon } from "lucide-react";Then replace the inline SVG in
MenuCheckboxItem(line 137-149) andMenuRadioItem(line 177-189) with:<CheckIcon />apps/web/src/components/ui/autocomplete.tsx (1)
64-73: Inline AutocompleteClear duplicates the standalone component's markup.The inline
AutocompleteClearrendered withinAutocompleteInput(lines 65-72) has similar but slightly different className than the standaloneAutocompleteClearcomponent (lines 238-254). Consider reusing the standalone component to reduce duplication and ensure consistent styling.♻️ Proposed refactor to reuse AutocompleteClear
{showClear && ( <AutocompleteClear className={cn( - "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", sizeValue === "sm" ? "end-0" : "end-0.5", )} - > - <XIcon /> - </AutocompleteClear> + /> )}Note: This requires ensuring
AutocompleteClearis defined beforeAutocompleteInputor hoisting the function definition.apps/web/src/components/ui/combobox.tsx (2)
11-17: Context type could use more precise RefObject typing.The
chipsRefin context is typed asReact.RefObject<Element | null>but is cast toReact.Ref<HTMLDivElement>when passed toComboboxChips. Consider typing the context more precisely to avoid the cast.♻️ Optional type refinement
const ComboboxContext = React.createContext<{ - chipsRef: React.RefObject<Element | null> | null; + chipsRef: React.RefObject<HTMLDivElement | null> | null; multiple: boolean; }>({ chipsRef: null, multiple: false, });Then line 307 can use
ref={chipsRef}without the cast.
30-53: Similar input patterns between ComboboxInput and AutocompleteInput suggest potential abstraction.
ComboboxInputandComboboxChipsInputshare significant structural similarity withAutocompleteInputfromautocomplete.tsx. If these patterns continue to proliferate, consider extracting a shared base input wrapper to reduce duplication.Also applies to: 55-119
apps/web/src/components/BranchToolbar.tsx (1)
207-213: Combobox is missing onValueChange handler for selection.The
Comboboxcomponent hasvalue={activeThread.branch}but noonValueChangehandler. The selection logic is handled viaonClickon individualComboboxItemcomponents (line 253). This works but bypasses the Combobox's built-in selection management. Consider usingonValueChangefor a more idiomatic implementation.♻️ Proposed refactor using onValueChange
<Combobox items={branchNames} autoHighlight onOpenChange={(open) => setIsBranchMenuOpen(open)} + onValueChange={(value) => { + const branch = branchByName.get(value); + if (branch) selectBranch(branch); + }} open={isBranchMenuOpen} value={activeThread.branch} >Then remove
onClickfromComboboxItem:<ComboboxItem hideIndicator key={branchName} value={branchName} className={branchName === activeThread.branch ? "bg-accent text-foreground" : undefined} - onClick={() => selectBranch(branch)} >
| import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID } from "./types"; | ||
| import { onServerWelcome } from "./wsNativeApi"; | ||
| import { useNativeApi } from "./hooks/useNativeApi"; | ||
| import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; |
There was a problem hiding this comment.
Route toast providers through packages/ui instead of local UI components.
This import bypasses the shared UI package; please move/re-export the toast providers from packages/ui and import from there to keep UI primitives centralized.
Based on learnings: Applies to apps/web/**/*.{ts,tsx} : Use packages/ui components - import UI components from the packages/ui package.
🤖 Prompt for AI Agents
In `@apps/web/src/App.tsx` at line 13, The import in App.tsx currently pulls
AnchoredToastProvider and ToastProvider from a local path; update the code to
use the centralized UI package by importing AnchoredToastProvider and
ToastProvider from "packages/ui" (or the published package name used in the
monorepo), and if they are not yet re-exported from packages/ui, add re-exports
there (export { AnchoredToastProvider, ToastProvider } from
"apps/web/src/components/ui/toast" or from their original module) so all apps
import these providers from the shared packages/ui entrypoint.
| import { cn } from "~/lib/utils"; | ||
| import { Input, type InputProps } from "~/components/ui/input"; | ||
| import { Textarea, type TextareaProps } from "~/components/ui/textarea"; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Switch Input/Textarea imports to packages/ui to keep the web UI on shared primitives.
These local imports bypass the shared UI package; please import from packages/ui (or move this component there and re-export) to avoid drift.
Based on learnings: "Use packages/ui components - import UI components from the packages/ui package".
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/input-group.tsx` around lines 6 - 8, The
InputGroup component currently imports Input, InputProps, Textarea, and
TextareaProps from local paths (and cn from utils); update the imports to use
the shared UI package instead by replacing the local imports of Input,
InputProps, Textarea, and TextareaProps with imports from "packages/ui" (or move
this component into the shared package and re-export), ensuring you still use
the existing symbols cn, Input, InputProps, Textarea, and TextareaProps where
referenced in the component.
| import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"; | ||
| import { XIcon } from "lucide-react"; | ||
| import { cn } from "~/lib/utils"; | ||
| import { Button } from "~/components/ui/button"; | ||
| import { ScrollArea } from "~/components/ui/scroll-area"; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/src/components/ui/sheet.tsxRepository: pingdotgg/codething-mvp
Length of output: 7435
Add missing React type import for ComponentProps usage throughout the file.
The file uses React.ComponentProps<"div"> on lines 109, 126, and 174 without importing React. This causes TypeScript compilation errors.
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
import { XIcon } from "lucide-react";
+import type * as React from "react";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui/button";
import { ScrollArea } from "~/components/ui/scroll-area";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"; | |
| import { XIcon } from "lucide-react"; | |
| import { cn } from "~/lib/utils"; | |
| import { Button } from "~/components/ui/button"; | |
| import { ScrollArea } from "~/components/ui/scroll-area"; | |
| import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"; | |
| import { XIcon } from "lucide-react"; | |
| import type * as React from "react"; | |
| import { cn } from "~/lib/utils"; | |
| import { Button } from "~/components/ui/button"; | |
| import { ScrollArea } from "~/components/ui/scroll-area"; |
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/sheet.tsx` around lines 3 - 8, Add the missing
React type import so usages of React.ComponentProps<"div"> compile: import React
(or import * as React) at the top of the file and ensure the file now references
React for the ComponentProps types used in the SheetPrimitive-related components
(where React.ComponentProps<"div"> appears around the
SheetRoot/SheetContent/SheetFooter definitions). No other code changes
required—just add the React import alongside the existing imports (e.g., with
the imports for SheetPrimitive, XIcon, cn, Button, ScrollArea) so TypeScript
recognizes React.ComponentProps.
| import { cn } from "~/lib/utils"; | ||
| import { Button } from "~/components/ui/button"; | ||
| import { ScrollArea } from "~/components/ui/scroll-area"; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Consume Button/ScrollArea from packages/ui per the shared UI guideline.
Based on learnings: "Use packages/ui components - import UI components from the packages/ui package".
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/sheet.tsx` around lines 5 - 7, Update the imports
in components/ui/sheet.tsx to consume shared UI components from the packages/ui
package: keep cn from "~/lib/utils" but change the Button and ScrollArea imports
(referenced as Button and ScrollArea in this file) to import from "packages/ui"
instead of "~/components/ui/button" and "~/components/ui/scroll-area"; adjust
any named import syntax accordingly so the rest of the file (e.g., usage of
Button and ScrollArea) remains unchanged.
| {toast.actionProps && ( | ||
| <Toast.Action | ||
| className={buttonVariants({ size: "xs" })} | ||
| data-slot="toast-action" | ||
| > | ||
| {toast.actionProps.children} | ||
| </Toast.Action> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/src/components/ui/toast.tsx | head -250Repository: pingdotgg/codething-mvp
Length of output: 14544
🏁 Script executed:
rg "actionProps" apps/web/src --type tsx -B 2 -A 2Repository: pingdotgg/codething-mvp
Length of output: 94
🏁 Script executed:
rg "actionProps" apps/web/src -B 2 -A 2Repository: pingdotgg/codething-mvp
Length of output: 2424
🏁 Script executed:
cat -n apps/web/src/components/GitActionsControl.tsx | grep -A 5 -B 5 "actionProps"Repository: pingdotgg/codething-mvp
Length of output: 853
🏁 Script executed:
fd "node_modules/@base-ui/react/toast" -type d 2>/dev/null | head -1 | xargs -I {} find {} -name "*.d.ts" -o -name "*.ts" | grep -i action | head -5Repository: pingdotgg/codething-mvp
Length of output: 49
🏁 Script executed:
rg "Toast\.Action" apps/web/src --type-list 2>/dev/null || rg "Toast\.Action" apps/web/srcRepository: pingdotgg/codething-mvp
Length of output: 354
🏁 Script executed:
cat apps/web/package.json | grep -A 2 -B 2 "@base-ui/react"Repository: pingdotgg/codething-mvp
Length of output: 204
🌐 Web query:
@base-ui/react Toast.Action component props documentation
💡 Result:
Toast.Action (from @base-ui/react/toast) performs an action when clicked and renders a <button> by default. (base-ui.com)
Props (API)
nativeButton?: boolean(default:true) — when usingrenderto replace the element, set tofalseif the replacement is not a<button>(e.g. a<div>). (base-ui.com)className?: string | ((state: Toast.Action.State) => string | undefined)(base-ui.com)style?: React.CSSProperties | ((state: Toast.Action.State) => React.CSSProperties | undefined)(base-ui.com)render?: ReactElement | ((props: HTMLProps, state: Toast.Action.State) => ReactElement)— replace/compose the underlying element. (base-ui.com)
Data attributes
data-type— the toast’s type. (base-ui.com)
Citations:
- 1: https://base-ui.com/react/components/toast?utm_source=openai
- 2: https://base-ui.com/react/components/toast?utm_source=openai
- 3: https://base-ui.com/react/components/toast?utm_source=openai
- 4: https://base-ui.com/react/components/toast?utm_source=openai
- 5: https://base-ui.com/react/components/toast?utm_source=openai
- 6: https://base-ui.com/react/components/toast?utm_source=openai
🌐 Web query:
@base-ui/react Toast.Action onClick event handler button
💡 Result:
In Base UI Toast, the recommended way to wire an onClick for the <Toast.Action /> button is to pass it via the toast’s actionProps when you create/update the toast (those props are applied to the Action button rendered inside that toast). (base-ui.com)
import { Toast } from '@base-ui/react/toast';
function SaveButton() {
const toastManager = Toast.useToastManager();
return (
<button
onClick={() => {
toastManager.add({
title: 'Saved',
description: 'Your changes were saved.',
actionProps: {
children: 'Undo',
onClick: () => {
// undo work here
},
},
});
}}
>
Save
</button>
);
}
// ...in your toast renderer:
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((toast) => (
<Toast.Root key={toast.id} toast={toast}>
<Toast.Content>
<Toast.Title />
<Toast.Description />
<Toast.Action /> {/* uses toast.actionProps */}
<Toast.Close />
</Toast.Content>
</Toast.Root>
));
}
``` ([base-ui.com](https://base-ui.com/react/components/toast))
Citations:
- 1: https://base-ui.com/react/components/toast
- 2: https://base-ui.com/react/components/toast
---
🌐 Web query:
`base-ui Toast.Action actionProps automatic spread render component`
💡 Result:
In Base UI, `actionProps` is **not** a prop you pass to `<Toast.Action />` directly. Instead, you attach it to the **toast object** when you create/update a toast (e.g. `toastManager.add({ actionProps: { children, onClick, ... } })`). Then, when that toast is rendered, `<Toast.Action />` will **automatically apply (“spread”) those `actionProps` onto its underlying `<button>`**, along with Base UI’s own required props/handlers. [1]
If you customize the element via the `render` prop on `<Toast.Action />`, Base UI gives you the computed `props` to apply—**you must spread those `props` onto whatever you render** (whether it’s a different tag or a composed component), otherwise the action won’t behave correctly. [1]
If your `render` target is *not* a real `<button>`, set `nativeButton={false}` on `<Toast.Action />`. [1]
**Source:** Base UI Toast docs (see `actionProps` in the toast object / “Undo action” example, and the `<Toast.Action />` API for `render` + `nativeButton`). [1]
References:
[1] https://base-ui.com/react/components/toast
---
🌐 Web query:
`base-ui Toast component actionProps automatic handling render example code`
💡 Result:
In Base UI **Toast**, you pass an action button’s props via the toast object’s `actionProps` option (e.g., for an “Undo” button). Then, when you render each toast, include `<Toast.Action />` inside `<Toast.Root />` and it will use those `actionProps`. [1]
Also note: swipe-to-dismiss is **automatically prevented for interactive elements** inside a toast; you can manually opt out with `data-swipe-ignore` if needed. [1]
```tsx
'use client';
import * as React from 'react';
import { Toast } from '@base-ui/react/toast';
export default function App() {
return (
<Toast.Provider>
<Composer />
<Toast.Portal>
<Toast.Viewport>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
);
}
function Composer() {
const toast = Toast.useToastManager();
function performAction() {
// do the thing...
toast.add({
title: 'Item deleted',
description: 'You can undo this.',
actionProps: {
children: 'Undo',
onClick: () => {
// undo the thing...
// optionally close the toast (if you kept the id)
},
},
});
}
return (
<button type="button" onClick={performAction}>
Perform action
</button>
);
}
function ToastList() {
const { toasts } = Toast.useToastManager();
return toasts.map((t) => (
<Toast.Root key={t.id} toast={t}>
<Toast.Content>
<Toast.Title />
<Toast.Description />
{/* Uses toast.actionProps (children, onClick, etc.) */}
<Toast.Action />
<Toast.Close aria-label="Close">×</Toast.Close>
</Toast.Content>
</Toast.Root>
));
}Custom rendering of the action button (via render)
If you want to render the action as your own component, use the render prop; if the rendered element isn’t a native <button>, set nativeButton={false}. [1]
<Toast.Action
nativeButton={false}
render={(props) => <a {...props} href="/support" />}
/>Sources: Base UI Toast docs (examples + API reference). [1]
[1] (base-ui.com)
Citations:
Forward toast.actionProps to enable action handler — onClick and other props are currently dropped.
toast.actionProps includes onClick, aria attributes, and other props passed when the toast is created, but the current rendering only extracts children and ignores the rest. This makes action buttons inert. Spread actionProps onto Toast.Action and merge the className with the existing buttonVariants styling.
🛠️ Proposed fix to forward action props
- {toast.actionProps && (
- <Toast.Action
- className={buttonVariants({ size: "xs" })}
- data-slot="toast-action"
- >
- {toast.actionProps.children}
- </Toast.Action>
- )}
+ {toast.actionProps && (
+ <Toast.Action
+ {...toast.actionProps}
+ className={cn(
+ buttonVariants({ size: "xs" }),
+ toast.actionProps.className,
+ )}
+ data-slot="toast-action"
+ >
+ {toast.actionProps.children}
+ </Toast.Action>
+ )}
- {toast.actionProps && (
- <Toast.Action
- className={buttonVariants({ size: "xs" })}
- data-slot="toast-action"
- >
- {toast.actionProps.children}
- </Toast.Action>
- )}
+ {toast.actionProps && (
+ <Toast.Action
+ {...toast.actionProps}
+ className={cn(
+ buttonVariants({ size: "xs" }),
+ toast.actionProps.className,
+ )}
+ data-slot="toast-action"
+ >
+ {toast.actionProps.children}
+ </Toast.Action>
+ )}Also applies to: lines 244–250
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/toast.tsx` around lines 152 - 158, The
Toast.Action currently renders only toast.actionProps.children and drops
actionable props (like onClick, aria-attributes); update the Toast.Action usage
to spread toast.actionProps onto the component (e.g., <Toast.Action
{...toast.actionProps} />) and merge/compose the className returned from
buttonVariants({ size: "xs" }) with any className present on toast.actionProps
so passed handlers and attributes are preserved; apply the same change for the
other instance that uses toast.actionProps (the block referenced at lines
244–250).
| import { type ClassValue, clsx } from "clsx"; | ||
| import { twMerge } from "tailwind-merge"; | ||
|
|
||
| export function cn(...inputs: ClassValue[]) { | ||
| return twMerge(clsx(inputs)); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if clsx and tailwind-merge are in apps/web/package.json
echo "=== Checking apps/web/package.json ==="
if [ -f "apps/web/package.json" ]; then
echo "Dependencies:"
cat apps/web/package.json | jq '.dependencies | keys | .[] | select(. == "clsx" or . == "tailwind-merge")'
echo ""
echo "Dev Dependencies:"
cat apps/web/package.json | jq '.devDependencies | keys | .[] | select(. == "clsx" or . == "tailwind-merge")'
echo ""
echo "Full dependencies output:"
cat apps/web/package.json | jq '.dependencies.clsx // .devDependencies.clsx // "NOT FOUND"' | head -1
cat apps/web/package.json | jq '.dependencies["tailwind-merge"] // .devDependencies["tailwind-merge"] // "NOT FOUND"' | head -1
else
echo "apps/web/package.json not found"
fi
# Also check the actual file to confirm it exists and has the expected content
echo ""
echo "=== Checking apps/web/src/lib/utils.ts ==="
if [ -f "apps/web/src/lib/utils.ts" ]; then
head -6 apps/web/src/lib/utils.ts
else
echo "File not found"
fiRepository: pingdotgg/codething-mvp
Length of output: 416
Add missing clsx dependency to apps/web/package.json.
The code imports clsx but the package is not in dependencies, which will cause a module resolution failure. Add clsx to the dependencies section of apps/web/package.json. The tailwind-merge package is already correctly installed.
🧰 Tools
🪛 GitHub Actions: CI
[error] 1-1: Cannot find module 'clsx' or its corresponding type declarations.bun run typecheck failed in '@t3tools/web'.
🤖 Prompt for AI Agents
In `@apps/web/src/lib/utils.ts` around lines 1 - 6, Add the missing clsx
dependency to the web app so the import in utils.ts (import { type ClassValue,
clsx } from "clsx"; and the cn(...inputs: ClassValue[]) function) resolves at
runtime: update apps/web/package.json dependencies to include a compatible
"clsx" entry (match the project’s package manager style and versioning) and run
install to ensure clsx is available for the cn function and any other imports.
- add working-tree file/line stats to git status contracts, server responses, and tests - refactor web git queries/mutations into shared React Query helpers - update git quick-action logic/UI to prefer commit/push/PR flows and clearer disabled states - hide branch toolbar for non-repos and tighten related chat/toolbar behavior
- Use the quick action button itself as the popover trigger - Keep hover tooltips working on disabled controls via pointer-events override - Remove focus/blur tab-stop handlers from disabled tooltip wrappers
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@apps/web/src/components/ChatView.tsx`:
- Around line 249-252: The BranchToolbar is currently shown because isGitRepo
defaults to true; change the guard so BranchToolbar only renders when gitCwd is
non-null and the branchesQuery succeeded and reports a repo (use gitCwd &&
branchesQuery.isSuccess && branchesQuery.data?.isRepo), and ensure branchesQuery
is created with useQuery(gitBranchesQueryOptions(api, gitCwd)) but treated as
disabled/false when gitCwd is null or the query errored (do not default
isGitRepo to true). Update any checks around BranchToolbar (e.g., where
BranchToolbar is rendered) to use this stricter condition to prevent rendering
in non‑repo contexts.
- Around line 895-923: The Button instances that currently use the disabled prop
(the ones calling onRespondToApproval with approval.requestId and using
isResponding) should switch to aria-disabled={isResponding}, appropriate
"disabled" visual styling (e.g., reduced opacity/disabled cursor classes), and a
click guard inside the onClick handlers so clicks are no-ops when isResponding
is true; update the four approval Buttons (those invoking
onRespondToApproval(..., "accept" | "acceptForSession" | "decline" | "cancel"))
to use aria-disabled instead of disabled, add styling classes to reflect the
disabled state, and wrap the handler call in a guard (if (isResponding) return)
or inline conditional before calling onRespondToApproval; apply the same pattern
to the runtime toggle and send button areas referenced in the comment.
🧹 Nitpick comments (2)
apps/web/src/components/GitActionsControl.tsx (1)
158-693: Consider splitting this component into subcomponents.
The file has grown large enough that extracting pieces like the quick-action button, menu, and dialog into local subcomponents would improve readability and reuse.As per coding guidelines: Extract large React components into multiple subcomponents with granular functionality. Co-locate subcomponents in the same file as the main component. Avoid hoisting callbacks too high up the component tree; prefer colocating logic close to JSX.
apps/server/src/git.ts (1)
282-294: Remove redundant first sort.The array is sorted at line 288 via
.toSorted(), then additional entries are pushed (lines 290-293), and then sorted again at line 294. The first sort is wasted work.♻️ Suggested simplification
let insertions = 0; let deletions = 0; const files = Array.from(fileStatMap.entries()) .map(([path, stat]) => { insertions += stat.insertions; deletions += stat.deletions; return { path, insertions: stat.insertions, deletions: stat.deletions }; - }) - .toSorted((a, b) => a.path.localeCompare(b.path)); + }); for (const filePath of changedFilesWithoutNumstat) { if (fileStatMap.has(filePath)) continue; files.push({ path: filePath, insertions: 0, deletions: 0 }); } files.sort((a, b) => a.path.localeCompare(b.path));
| const branchesQuery = useQuery(gitBranchesQueryOptions(api, gitCwd)); | ||
| // Default true while loading to avoid toolbar flicker. | ||
| const isGitRepo = branchesQuery.data?.isRepo ?? true; | ||
|
|
There was a problem hiding this comment.
Guard BranchToolbar when gitCwd/query is unavailable.
isGitRepo defaults to true, so if gitCwd is null, the query is disabled, or the query errors, the toolbar can still render in non‑repo contexts. Tighten the guard so non‑repos never show it.
🔧 Suggested fix
- const branchesQuery = useQuery(gitBranchesQueryOptions(api, gitCwd));
- // Default true while loading to avoid toolbar flicker.
- const isGitRepo = branchesQuery.data?.isRepo ?? true;
+ const branchesQuery = useQuery(gitBranchesQueryOptions(api, gitCwd));
+ const isGitRepo =
+ Boolean(gitCwd) &&
+ (branchesQuery.isLoading ? true : branchesQuery.data?.isRepo ?? false);Also applies to: 1307-1307
🤖 Prompt for AI Agents
In `@apps/web/src/components/ChatView.tsx` around lines 249 - 252, The
BranchToolbar is currently shown because isGitRepo defaults to true; change the
guard so BranchToolbar only renders when gitCwd is non-null and the
branchesQuery succeeded and reports a repo (use gitCwd &&
branchesQuery.isSuccess && branchesQuery.data?.isRepo), and ensure branchesQuery
is created with useQuery(gitBranchesQueryOptions(api, gitCwd)) but treated as
disabled/false when gitCwd is null or the query errored (do not default
isGitRepo to true). Update any checks around BranchToolbar (e.g., where
BranchToolbar is rendered) to use this stricter condition to prevent rendering
in non‑repo contexts.
| <Button | ||
| size="xs" | ||
| variant="default" | ||
| disabled={isResponding} | ||
| onClick={() => void onRespondToApproval(approval.requestId, "accept")} | ||
| > | ||
| Approve once | ||
| </Button> | ||
| <Button | ||
| size="xs" | ||
| variant="outline" | ||
| disabled={isResponding} | ||
| onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")} | ||
| > | ||
| Always allow this session | ||
| </Button> | ||
| <Button | ||
| size="xs" | ||
| variant="destructive-outline" | ||
| disabled={isResponding} | ||
| onClick={() => void onRespondToApproval(approval.requestId, "decline")} | ||
| > | ||
| Decline | ||
| </Button> | ||
| <Button | ||
| size="xs" | ||
| variant="ghost" | ||
| disabled={isResponding} | ||
| onClick={() => void onRespondToApproval(approval.requestId, "cancel")} |
There was a problem hiding this comment.
Avoid disabled on buttons; use aria-disabled + click guards.
The approvals actions, runtime toggle, and send button all use disabled, which breaks tooltips and harms accessibility per repo guidance. Replace with aria-disabled, styling, and a click guard.
♿ Example pattern (apply to all three areas)
- <Button
- size="xs"
- variant="default"
- disabled={isResponding}
- onClick={() => void onRespondToApproval(approval.requestId, "accept")}
- >
+ <Button
+ size="xs"
+ variant="default"
+ aria-disabled={isResponding}
+ className={cn(isResponding && "opacity-50")}
+ onClick={() => {
+ if (isResponding) return;
+ void onRespondToApproval(approval.requestId, "accept");
+ }}
+ >Based on learnings: "Avoid using disabled props on buttons as they harm accessibility and break tooltips. Instead, use styling (e.g., opacity, hover states) and handle the disabled state through click handlers."
Also applies to: 1211-1229, 1253-1258
🤖 Prompt for AI Agents
In `@apps/web/src/components/ChatView.tsx` around lines 895 - 923, The Button
instances that currently use the disabled prop (the ones calling
onRespondToApproval with approval.requestId and using isResponding) should
switch to aria-disabled={isResponding}, appropriate "disabled" visual styling
(e.g., reduced opacity/disabled cursor classes), and a click guard inside the
onClick handlers so clicks are no-ops when isResponding is true; update the four
approval Buttons (those invoking onRespondToApproval(..., "accept" |
"acceptForSession" | "decline" | "cancel")) to use aria-disabled instead of
disabled, add styling classes to reflect the disabled state, and wrap the
handler call in a guard (if (isResponding) return) or inline conditional before
calling onRespondToApproval; apply the same pattern to the runtime toggle and
send button areas referenced in the comment.
| setGitModalResult(null); | ||
| }, [isGitModalActionRunning]); | ||
| let stageIndex = 0; | ||
| const stageInterval = setInterval(() => { |
There was a problem hiding this comment.
🟢 Low
components/GitActionsControl.tsx:264 Suggestion: ensure unmount‑safety. runGitActionWithToast creates a setInterval and toast action callbacks that can outlive the component, risking leaks and setState‑after‑unmount warnings. Consider storing the timer and any created toast IDs in refs and clearing/dismissing them in a useEffect cleanup, and guard setActiveDialogAction with an isMounted ref (or avoid using component state in toast actions).
🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/web/src/components/GitActionsControl.tsx around line 264:
Suggestion: ensure unmount‑safety. `runGitActionWithToast` creates a `setInterval` and toast action callbacks that can outlive the component, risking leaks and `setState`‑after‑unmount warnings. Consider storing the timer and any created toast IDs in refs and clearing/dismissing them in a `useEffect` cleanup, and guard `setActiveDialogAction` with an `isMounted` ref (or avoid using component state in toast actions).
| const [statusStdout, unstagedNumstatStdout, stagedNumstatStdout] = await Promise.all([ | ||
| this.gitStdout(cwd, ["status", "--porcelain=2", "--branch"]), | ||
| this.gitStdout(cwd, ["diff", "--numstat"]), | ||
| this.gitStdout(cwd, ["diff", "--cached", "--numstat"]), | ||
| ]); |
There was a problem hiding this comment.
🟡 Medium
src/git.ts:233 Running git status and git diff commands concurrently via Promise.all causes index lock contention—both commands try to acquire .git/index.lock, causing intermittent Unable to create index.lock failures. Consider running these commands sequentially, or adding --no-refresh flags to the diff commands.
- const [statusStdout, unstagedNumstatStdout, stagedNumstatStdout] = await Promise.all([
- this.gitStdout(cwd, ["status", "--porcelain=2", "--branch"]),
- this.gitStdout(cwd, ["diff", "--numstat"]),
- this.gitStdout(cwd, ["diff", "--cached", "--numstat"]),
- ]);
+ const [statusStdout, unstagedNumstatStdout, stagedNumstatStdout] = await Promise.all([
+ this.gitStdout(cwd, ["status", "--porcelain=2", "--branch"]),
+ this.gitStdout(cwd, ["diff", "--no-refresh", "--numstat"]),
+ this.gitStdout(cwd, ["diff", "--no-refresh", "--cached", "--numstat"]),
+ ]);🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/server/src/git.ts around lines 233-237:
Running `git status` and `git diff` commands concurrently via `Promise.all` causes index lock contention—both commands try to acquire `.git/index.lock`, causing intermittent `Unable to create index.lock` failures. Consider running these commands sequentially, or adding `--no-refresh` flags to the `diff` commands.
- replace generic success text with action-specific toast titles/descriptions - include short SHA/branch details for push and commit outcomes - add tests for PR/commit/push summaries and description truncation
- show push target as `origin/<branch>` when available - update summarizeGitResult test expectation accordingly
- Shorten success notification timeout in `GitActionsControl` from 10s to 5s
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@apps/web/src/components/GitActionsControl.tsx`:
- Around line 390-405: runDialogAction currently submits using
dialogCommitMessage but never clears it afterwards, causing the text to persist
into subsequent dialogs; update runDialogAction to call the state setter that
clears the commit input (e.g., setDialogCommitMessage("")) immediately after
invoking runGitActionWithToast (or after setActiveDialogAction(null)) so the
dialogCommitMessage state is reset for the next open; reference runDialogAction
and dialogCommitMessage (and the setter setDialogCommitMessage) when making this
change.
- Around line 293-311: The CTA in the success toast currently calls
api?.shell.openExternal(prUrl) without handling rejections; update the branch
that supplies the "Open PR" actionProps inside toastManager.update to call the
same safe helper pattern used by openExistingPr (or inline the same logic) so
the promise from api.shell.openExternal is awaited/then/catch-handled and errors
surface to the user (e.g., update toast or show error toast) instead of
producing an unhandled rejection; locate the CTA setup in toastManager.update
and replace the raw api?.shell.openExternal(prUrl) call with the guarded call
used in openExistingPr.
🧹 Nitpick comments (1)
apps/web/src/components/GitActionsControl.tsx (1)
445-684: Consider splitting GitActionsControl into subcomponents.
This JSX block is sizable; extracting the quick-action button, menu, and dialog/status card into co-located subcomponents would reduce cognitive load and keep state closer to the UI.Based on learnings: “Extract large React components into multiple subcomponents with granular functionality. Co-locate subcomponents in the same file as the main component. Avoid hoisting callbacks too high up the component tree; prefer colocating logic close to JSX.”
| toastManager.update(progressToastId, { | ||
| type: "success", | ||
| title: "Done", | ||
| description: describeGitResult(result), | ||
| timeout: 10_000, | ||
| ...(shouldOfferPushCta | ||
| ? { | ||
| actionProps: { | ||
| children: "Push", | ||
| onClick: () => setActiveDialogAction("push"), | ||
| }, | ||
| } | ||
| : shouldOfferOpenPrCta | ||
| ? { | ||
| actionProps: { | ||
| children: "Open PR", | ||
| onClick: () => void api?.shell.openExternal(prUrl), | ||
| }, | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "openExternal" -SRepository: pingdotgg/codething-mvp
Length of output: 968
🏁 Script executed:
# Find the openExistingPr implementation and error handling
rg -n "openExistingPr" --type ts --type tsx -A 20 | head -100Repository: pingdotgg/codething-mvp
Length of output: 94
🏁 Script executed:
# Check the git actions file to see the current implementation
cat -n apps/web/src/components/GitActionsControl.tsx | sed -n '280,320p'Repository: pingdotgg/codething-mvp
Length of output: 1812
🏁 Script executed:
# Find the openExistingPr function to see the error handling pattern
cat -n apps/web/src/components/GitActionsControl.tsx | sed -n '220,260p'Repository: pingdotgg/codething-mvp
Length of output: 1669
🏁 Script executed:
# Check ThreadTerminalDrawer to see another example of proper error handling
cat -n apps/web/src/components/ThreadTerminalDrawer.tsx | sed -n '215,230p'Repository: pingdotgg/codething-mvp
Length of output: 778
🏁 Script executed:
# Check the toastManager implementation to see if it has a promise method
rg -n "toastManager\.promise" --type ts -B 2 -A 5 | head -80Repository: pingdotgg/codething-mvp
Length of output: 1536
Handle failures from the PR CTA link opening.
The success toast CTA calls api?.shell.openExternal(prUrl) without handling rejections, which can surface as unhandled promise rejections and leave users without feedback. The openExistingPr handler (line 220) demonstrates the proper pattern already established in this file.
🔧 Suggested fix
- onClick: () => void api?.shell.openExternal(prUrl),
+ onClick: () => {
+ const promise = api?.shell.openExternal(prUrl);
+ if (!promise) return;
+ toastManager.promise(promise, {
+ loading: { title: "Opening PR..." },
+ success: { title: "PR opened" },
+ error: (err) => ({
+ title: "Unable to open PR link",
+ description: err instanceof Error ? err.message : "An error occurred.",
+ }),
+ });
+ void promise.catch(() => undefined);
+ },🤖 Prompt for AI Agents
In `@apps/web/src/components/GitActionsControl.tsx` around lines 293 - 311, The
CTA in the success toast currently calls api?.shell.openExternal(prUrl) without
handling rejections; update the branch that supplies the "Open PR" actionProps
inside toastManager.update to call the same safe helper pattern used by
openExistingPr (or inline the same logic) so the promise from
api.shell.openExternal is awaited/then/catch-handled and errors surface to the
user (e.g., update toast or show error toast) instead of producing an unhandled
rejection; locate the CTA setup in toastManager.update and replace the raw
api?.shell.openExternal(prUrl) call with the guarded call used in
openExistingPr.
| const runDialogAction = useCallback(() => { | ||
| if (!activeDialogAction) return; | ||
| const action: GitStackedAction = | ||
| activeDialogAction === "commit" | ||
| ? "commit" | ||
| : activeDialogAction === "push" | ||
| ? "commit_push" | ||
| : "commit_push_pr"; | ||
| const commitMessage = | ||
| activeDialogAction === "commit" ? dialogCommitMessage.trim() : ""; | ||
| setActiveDialogAction(null); | ||
| void runGitActionWithToast({ | ||
| action, | ||
| ...(commitMessage ? { commitMessage } : {}), | ||
| }); | ||
| }, [activeDialogAction, dialogCommitMessage, runGitActionWithToast]); |
There was a problem hiding this comment.
Clear the commit message after submitting an action.
dialogCommitMessage is only reset on cancel/onOpenChange, so it can persist into the next commit after a successful submit.
🧹 Suggested fix
setActiveDialogAction(null);
+ setDialogCommitMessage("");
void runGitActionWithToast({🤖 Prompt for AI Agents
In `@apps/web/src/components/GitActionsControl.tsx` around lines 390 - 405,
runDialogAction currently submits using dialogCommitMessage but never clears it
afterwards, causing the text to persist into subsequent dialogs; update
runDialogAction to call the state setter that clears the commit input (e.g.,
setDialogCommitMessage("")) immediately after invoking runGitActionWithToast (or
after setActiveDialogAction(null)) so the dialogCommitMessage state is reset for
the next open; reference runDialogAction and dialogCommitMessage (and the setter
setDialogCommitMessage) when making this change.
| }; | ||
| } | ||
|
|
||
| if (hasChanges) { |
There was a problem hiding this comment.
🟡 Medium
components/GitActionsControl.logic.ts:167 Consider checking isBehind before returning commit_push actions in the hasChanges block. Currently, if the branch has uncommitted changes and is behind remote, this returns a push action that will fail (non-fast-forward). The isBehind check should come first, or be added as a condition here.
🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/web/src/components/GitActionsControl.logic.ts around line 167:
Consider checking `isBehind` before returning `commit_push` actions in the `hasChanges` block. Currently, if the branch has uncommitted changes *and* is behind remote, this returns a push action that will fail (non-fast-forward). The `isBehind` check should come first, or be added as a condition here.
| label: "Commit, push & create PR", | ||
| disabled: false, | ||
| kind: "run_action", | ||
| action: "commit_push_pr", |
There was a problem hiding this comment.
🟡 Medium
components/GitActionsControl.logic.ts:175 commit_push_pr is returned without checking gitStatus.hasUpstream, but buildMenuItems requires hasUpstream for PR creation. Consider adding the same check here to avoid failing PR creation on branches without upstream.
🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/web/src/components/GitActionsControl.logic.ts around line 175:
`commit_push_pr` is returned without checking `gitStatus.hasUpstream`, but `buildMenuItems` requires `hasUpstream` for PR creation. Consider adding the same check here to avoid failing PR creation on branches without upstream.
| data-align={align} | ||
| data-slot="input-group-addon" | ||
| onMouseDown={(e) => { | ||
| const target = e.target as HTMLElement; |
There was a problem hiding this comment.
🟢 Low
ui/input-group.tsx:56 The cast to HTMLElement assumes e.target always has .closest(), but in some edge cases the target could be a non-Element node. Consider guarding with if (!(target instanceof Element)) return; before calling .closest(), or this might be intentionally relying on React's synthetic events always providing Elements.
| const target = e.target as HTMLElement; | |
| const target = e.target as HTMLElement; | |
| if (!(target instanceof Element)) return; |
🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/web/src/components/ui/input-group.tsx around line 56:
The cast to `HTMLElement` assumes `e.target` always has `.closest()`, but in some edge cases the target could be a non-Element node. Consider guarding with `if (!(target instanceof Element)) return;` before calling `.closest()`, or this might be intentionally relying on React's synthetic events always providing Elements.
| className, | ||
| )} | ||
| data-slot="separator" | ||
| orientation={orientation} |
There was a problem hiding this comment.
🟢 Low
ui/separator.tsx:13 The not-[[class^='h-']] selector won't work because cn prepends base classes like shrink-0, so h-* classes never appear at the start of the class string. Consider using not-[[class*='h-']] (contains) instead of [class^='h-'] (starts with).
| orientation={orientation} | |
| "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:not-[[class*=' h-']]:not-[[class^='h-']]:not-[[class*='_h-']]:self-stretch", |
🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/web/src/components/ui/separator.tsx around line 13:
The `not-[[class^='h-']]` selector won't work because `cn` prepends base classes like `shrink-0`, so `h-*` classes never appear at the start of the class string. Consider using `not-[[class*='h-']]` (contains) instead of `[class^='h-']` (starts with).
- Trigger `commit_push` directly from primary Push actions - Simplify push confirmation copy and dialog descriptions - Replace 6-file cap with a scrollable full changed-files list plus totals
- Only include commit-related progress steps for stacked actions when there are working tree changes - Prevents showing "Generating commit message"/"Committing" during pure push or PR flows
- set success toast timeout in `GitActionsControl` from `5_000` to `0` - keeps post-action feedback and push CTA available until user closes it
- Dismiss the success toast before handling Push/Open PR/Create PR CTA clicks - Prevent stale result toasts from lingering while starting the next git action
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@apps/web/src/components/ChatView.tsx`:
- Around line 1461-1474: The keyboard handler inside the useEffect (the handler
function) calls api.shell.openInEditor directly and therefore bypasses the same
local state/localStorage update performed by the openInEditor callback; update
the handler to mirror openInEditor: call setLastEditor(newEditor) and persist
the value to localStorage (same key used elsewhere, e.g., LAST_EDITOR_KEY)
before invoking api.shell.openInEditor(cwd, newEditor) so lastEditor state and
storage stay consistent with button-driven opens.
In `@apps/web/src/components/ui/alert-dialog.tsx`:
- Around line 1-5: The file uses React.ComponentProps<"div"> (seen on the props
types at lines using those type expressions), but React is not imported; add an
import for React at the top of the file (e.g., import React from "react" or
import * as React from "react") so the type React.ComponentProps is available;
update the top alongside the existing imports (near AlertDialogPrimitive and cn)
to fix the TypeScript errors referencing React.ComponentProps<"div">.
In `@apps/web/src/components/ui/card.tsx`:
- Around line 1-196: The Card primitives (Card, CardFrame, CardHeader,
CardTitle, CardDescription, CardAction, CardPanel, CardFooter, CardFrameHeader,
CardFrameTitle, CardFrameDescription, CardFrameFooter) were implemented inside
apps/web and must be moved to the shared UI package; extract the entire file
contents into packages/ui as a new module (e.g.,
packages/ui/src/components/card.tsx), ensure the module exports the same named
symbols (Card, CardFrame, CardFrameHeader, CardFrameTitle, CardFrameDescription,
CardFrameFooter, CardHeader, CardTitle, CardDescription, CardAction, CardPanel,
CardContent alias, CardFooter), add that export to packages/ui's public exports
(index barrel), then update the original apps/web imports to import these
components from packages/ui instead of the local file; keep the same function
names and prop types (useRender.ComponentProps<"div">) so callers continue to
work.
In `@apps/web/src/components/ui/input.tsx`:
- Around line 1-62: Move the Input component (Input, InputProps, and its use of
InputPrimitive/nativeInput) out of the app and into the shared UI package
(packages/ui) and re-export it from that package; update the package's public
exports (the UI package index/exports) to include Input, adjust all app imports
to import Input from the shared UI package instead of the app path, and ensure
TypeScript types and any peer deps (base-ui/react) are declared in the UI
package so the component builds correctly.
- Around line 8-57: The Input component declares refs for HTMLInputElement but
currently wraps the input in a span so incoming refs attach to the span; convert
the Input function into a React.forwardRef component (export default
React.forwardRef or similar) that accepts a ref parameter typed as
HTMLInputElement and pass that ref through to the actual input element (for
nativeInput case) or to the underlying InputPrimitive (for non-native case),
keeping the existing size prop handling and props spread; update InputProps to
remove/react-adjust RefAttributes if needed so the forwarded ref type matches
the component signature and ensure the span remains without receiving the
forwarded ref.
🧹 Nitpick comments (9)
apps/web/src/components/ChatView.tsx (1)
7-7: ImportModelSlugas a type.
ModelSlugis only used as a type annotation (lines 798, 1375). Import it with thetypekeyword for proper tree-shaking.♻️ Suggested fix
import { PROVIDER_SEND_TURN_MAX_INPUT_CHARS, PROVIDER_SEND_TURN_MAX_ATTACHMENTS, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, type ProviderSendTurnAttachmentInput, type ProviderApprovalDecision, - ModelSlug, + type ModelSlug, } from "@t3tools/contracts";apps/web/src/components/ui/input-group.tsx (1)
45-70: ComposeonMouseDownwith consumer handlers.Right now a consumer-provided
onMouseDown(via props) will override this logic or vice‑versa. Composing both handlers preserves your focus behavior while still honoring consumer hooks.♻️ Suggested refactor
-function InputGroupAddon({ - className, - align = "inline-start", - ...props -}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) { +function InputGroupAddon({ + className, + align = "inline-start", + onMouseDown, + ...props +}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) { return ( <div className={cn(inputGroupAddonVariants({ align }), className)} data-align={align} data-slot="input-group-addon" - onMouseDown={(e) => { + onMouseDown={(e) => { + onMouseDown?.(e); + if (e.defaultPrevented) return; const target = e.target as HTMLElement; const isInteractive = target.closest( "button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']", ); if (isInteractive) return; e.preventDefault(); const parent = e.currentTarget.parentElement; const input = parent?.querySelector<HTMLInputElement | HTMLTextAreaElement>( "input, textarea", ); if (input && !parent?.querySelector("input:focus, textarea:focus")) { input.focus(); } }} {...props} /> ); }apps/web/src/components/ui/scroll-area.tsx (1)
7-35: Prefer sourcing ScrollArea frompackages/ui.
This local component inapps/webcan drift from the shared UI layer. Consider moving it intopackages/uiand importing it from there so the web app consumes the shared component consistently.Based on learnings: Applies to apps/web/**/*.{ts,tsx} : Use packages/ui components - import UI components from the packages/ui package.
apps/web/src/components/BranchToolbar.tsx (3)
2-3: Consider namespace React import for consistency.
The repo prefersimport * as ReactandReact.useState/useEffectover named React imports.Based on learnings: "Prefer namespace imports in TypeScript (e.g.,
import * as React from 'react',import * as Effect from 'effect/Effect')".
12-23: Consider consuming UI primitives frompackages/ui.
This file imports Button/Combobox/InputGroup from local paths; the shared UI guideline suggests using the package export when available (or re-exporting these there).Based on learnings: "Use packages/ui components - import UI components from the packages/ui package".
159-293: Consider splittingBranchToolbarinto subcomponents.
The JSX flow (env toggle, combobox list, and create-branch form) is large enough to benefit from co-located subcomponents for readability and testability.Based on learnings: "Extract large React components into multiple subcomponents with granular functionality. Co-locate subcomponents in the same file as the main component. Avoid hoisting callbacks too high up the component tree; prefer colocating logic close to JSX."
apps/web/src/components/ui/command.tsx (1)
7-17: Prefer shared UI exports for Autocomplete (and related primitives).
To align with the shared UI guideline, consider re-exporting Autocomplete primitives frompackages/uiand importing from there instead of local paths.Based on learnings: "Use packages/ui components - import UI components from the packages/ui package".
apps/web/src/components/GitActionsControl.tsx (1)
253-265: Consider clearing the interval on component unmount.The interval is cleared in both success and error branches, but if the component unmounts during a pending action, the interval may continue running. This is unlikely in practice since the action is awaited, but for robustness you could use
useEffectcleanup or an abort pattern.apps/server/src/git.ts (1)
283-295: Minor: Consider sorting once after all files are added.The code sorts at line 289 with
.toSorted()then potentially adds more files (lines 291-294) and sorts again at line 295. You could simplify by collecting all files first and sorting once.♻️ Suggested optimization
- const files = Array.from(fileStatMap.entries()) - .map(([path, stat]) => { - insertions += stat.insertions; - deletions += stat.deletions; - return { path, insertions: stat.insertions, deletions: stat.deletions }; - }) - .toSorted((a, b) => a.path.localeCompare(b.path)); - - for (const filePath of changedFilesWithoutNumstat) { - if (fileStatMap.has(filePath)) continue; - files.push({ path: filePath, insertions: 0, deletions: 0 }); - } - files.sort((a, b) => a.path.localeCompare(b.path)); + const files: Array<{ path: string; insertions: number; deletions: number }> = []; + for (const [path, stat] of fileStatMap.entries()) { + insertions += stat.insertions; + deletions += stat.deletions; + files.push({ path, insertions: stat.insertions, deletions: stat.deletions }); + } + for (const filePath of changedFilesWithoutNumstat) { + if (fileStatMap.has(filePath)) continue; + files.push({ path: filePath, insertions: 0, deletions: 0 }); + } + files.sort((a, b) => a.path.localeCompare(b.path));
| // Cmd+O / Ctrl+O to open in last-used editor | ||
| useEffect(() => { | ||
| const handler = (e: globalThis.KeyboardEvent) => { | ||
| if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) { | ||
| if (api && activeProject) { | ||
| e.preventDefault(); | ||
| const cwd = activeThread?.worktreePath ?? activeProject.cwd; | ||
| void api.shell.openInEditor(cwd, lastEditor); | ||
| } | ||
| } | ||
| }; | ||
| window.addEventListener("keydown", handler); | ||
| return () => window.removeEventListener("keydown", handler); | ||
| }, [api, activeProject, activeThread, lastEditor]); |
There was a problem hiding this comment.
Keyboard handler bypasses setLastEditor update.
The Cmd/Ctrl+O handler directly calls api.shell.openInEditor without updating localStorage or lastEditor state, unlike the openInEditor callback used by button clicks. This creates inconsistent behavior.
🔧 Suggested fix
useEffect(() => {
const handler = (e: globalThis.KeyboardEvent) => {
if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
- if (api && activeProject) {
+ if (activeProject) {
e.preventDefault();
- const cwd = activeThread?.worktreePath ?? activeProject.cwd;
- void api.shell.openInEditor(cwd, lastEditor);
+ openInEditor(lastEditor);
}
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
- }, [api, activeProject, activeThread, lastEditor]);
+ }, [activeProject, openInEditor, lastEditor]);🤖 Prompt for AI Agents
In `@apps/web/src/components/ChatView.tsx` around lines 1461 - 1474, The keyboard
handler inside the useEffect (the handler function) calls api.shell.openInEditor
directly and therefore bypasses the same local state/localStorage update
performed by the openInEditor callback; update the handler to mirror
openInEditor: call setLastEditor(newEditor) and persist the value to
localStorage (same key used elsewhere, e.g., LAST_EDITOR_KEY) before invoking
api.shell.openInEditor(cwd, newEditor) so lastEditor state and storage stay
consistent with button-driven opens.
| "use client"; | ||
|
|
||
| import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; | ||
|
|
||
| import { cn } from "~/lib/utils"; |
There was a problem hiding this comment.
Missing React import for type usage.
React.ComponentProps<"div"> is used on lines 71 and 85 but React is not imported. This will cause a TypeScript error.
🐛 Proposed fix
"use client";
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
+import type * as React from "react";
import { cn } from "~/lib/utils";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "use client"; | |
| import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; | |
| import { cn } from "~/lib/utils"; | |
| "use client"; | |
| import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; | |
| import type * as React from "react"; | |
| import { cn } from "~/lib/utils"; |
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/alert-dialog.tsx` around lines 1 - 5, The file
uses React.ComponentProps<"div"> (seen on the props types at lines using those
type expressions), but React is not imported; add an import for React at the top
of the file (e.g., import React from "react" or import * as React from "react")
so the type React.ComponentProps is available; update the top alongside the
existing imports (near AlertDialogPrimitive and cn) to fix the TypeScript errors
referencing React.ComponentProps<"div">.
| "use client"; | ||
|
|
||
| import { mergeProps } from "@base-ui/react/merge-props"; | ||
| import { useRender } from "@base-ui/react/use-render"; | ||
|
|
||
| import { cn } from "~/lib/utils"; | ||
|
|
||
| function Card({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn( | ||
| "relative flex flex-col rounded-2xl border bg-card not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | ||
| className, | ||
| ), | ||
| "data-slot": "card", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardFrame({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn( | ||
| "[--clip-top:-1rem] [--clip-bottom:-1rem] *:data-[slot=card]:first:[--clip-top:1px] *:data-[slot=card]:last:[--clip-bottom:1px] flex flex-col relative rounded-2xl border bg-card before:bg-muted/72 not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] *:data-[slot=card]:-m-px *:not-last:data-[slot=card]:rounded-b-xl *:not-last:data-[slot=card]:before:rounded-b-[calc(var(--radius-xl)-1px)] *:not-first:data-[slot=card]:rounded-t-xl *:not-first:data-[slot=card]:before:rounded-t-[calc(var(--radius-xl)-1px)] *:data-[slot=card]:[clip-path:inset(var(--clip-top)_1px_var(--clip-bottom)_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:shadow-none *:data-[slot=card]:before:hidden *:data-[slot=card]:bg-clip-padding", | ||
| className, | ||
| ), | ||
| "data-slot": "card-frame", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardFrameHeader({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn("relative flex flex-col px-6 py-4", className), | ||
| "data-slot": "card-frame-header", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardFrameTitle({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn("font-semibold text-sm", className), | ||
| "data-slot": "card-frame-title", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardFrameDescription({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn("text-muted-foreground text-sm", className), | ||
| "data-slot": "card-frame-description", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardFrameFooter({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn("px-6 py-4", className), | ||
| "data-slot": "card-frame-footer", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardHeader({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn( | ||
| "grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pb-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]", | ||
| className, | ||
| ), | ||
| "data-slot": "card-header", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardTitle({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn("font-semibold text-lg leading-none", className), | ||
| "data-slot": "card-title", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardDescription({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn("text-muted-foreground text-sm", className), | ||
| "data-slot": "card-description", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardAction({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn( | ||
| "col-start-2 row-span-2 row-start-1 self-start justify-self-end inline-flex", | ||
| className, | ||
| ), | ||
| "data-slot": "card-action", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardPanel({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn( | ||
| "flex-1 p-6 in-[[data-slot=card]:has(>[data-slot=card-header]:not(.border-b))]:pt-0 in-[[data-slot=card]:has(>[data-slot=card-footer]:not(.border-t))]:pb-0", | ||
| className, | ||
| ), | ||
| "data-slot": "card-panel", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| function CardFooter({ className, render, ...props }: useRender.ComponentProps<"div">) { | ||
| const defaultProps = { | ||
| className: cn( | ||
| "flex items-center p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pt-4", | ||
| className, | ||
| ), | ||
| "data-slot": "card-footer", | ||
| }; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: "div", | ||
| props: mergeProps<"div">(defaultProps, props), | ||
| render, | ||
| }); | ||
| } | ||
|
|
||
| export { | ||
| Card, | ||
| CardFrame, | ||
| CardFrameHeader, | ||
| CardFrameTitle, | ||
| CardFrameDescription, | ||
| CardFrameFooter, | ||
| CardAction, | ||
| CardDescription, | ||
| CardFooter, | ||
| CardHeader, | ||
| CardPanel, | ||
| CardPanel as CardContent, | ||
| CardTitle, | ||
| }; |
There was a problem hiding this comment.
Move Card primitives into packages/ui and import from there.
This file defines UI primitives directly inside apps/web, which conflicts with the shared UI sourcing rule. Please relocate these components into packages/ui (or add them there if missing) and consume them via imports in the web app.
As per coding guidelines: "apps/web/**/*.{ts,tsx}: Use packages/ui components - import UI components from the packages/ui package".
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/card.tsx` around lines 1 - 196, The Card
primitives (Card, CardFrame, CardHeader, CardTitle, CardDescription, CardAction,
CardPanel, CardFooter, CardFrameHeader, CardFrameTitle, CardFrameDescription,
CardFrameFooter) were implemented inside apps/web and must be moved to the
shared UI package; extract the entire file contents into packages/ui as a new
module (e.g., packages/ui/src/components/card.tsx), ensure the module exports
the same named symbols (Card, CardFrame, CardFrameHeader, CardFrameTitle,
CardFrameDescription, CardFrameFooter, CardHeader, CardTitle, CardDescription,
CardAction, CardPanel, CardContent alias, CardFooter), add that export to
packages/ui's public exports (index barrel), then update the original apps/web
imports to import these components from packages/ui instead of the local file;
keep the same function names and prop types (useRender.ComponentProps<"div">) so
callers continue to work.
| "use client"; | ||
|
|
||
| import { Input as InputPrimitive } from "@base-ui/react/input"; | ||
| import type * as React from "react"; | ||
|
|
||
| import { cn } from "~/lib/utils"; | ||
|
|
||
| type InputProps = Omit<InputPrimitive.Props & React.RefAttributes<HTMLInputElement>, "size"> & { | ||
| size?: "sm" | "default" | "lg" | number; | ||
| unstyled?: boolean; | ||
| nativeInput?: boolean; | ||
| }; | ||
|
|
||
| function Input({ | ||
| className, | ||
| size = "default", | ||
| unstyled = false, | ||
| nativeInput = false, | ||
| ...props | ||
| }: InputProps) { | ||
| const inputClassName = cn( | ||
| "h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none placeholder:text-muted-foreground/72 sm:h-7.5 sm:leading-7.5 [transition:background-color_5000000s_ease-in-out_0s]", | ||
| size === "sm" && "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5", | ||
| size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5", | ||
| props.type === "search" && | ||
| "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none", | ||
| props.type === "file" && | ||
| "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm", | ||
| ); | ||
|
|
||
| return ( | ||
| <span | ||
| className={ | ||
| cn( | ||
| !unstyled && | ||
| "relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]", | ||
| className, | ||
| ) || undefined | ||
| } | ||
| data-size={size} | ||
| data-slot="input-control" | ||
| > | ||
| {nativeInput ? ( | ||
| <input | ||
| className={inputClassName} | ||
| data-slot="input" | ||
| size={typeof size === "number" ? size : undefined} | ||
| {...props} | ||
| /> | ||
| ) : ( | ||
| <InputPrimitive | ||
| className={inputClassName} | ||
| data-slot="input" | ||
| size={typeof size === "number" ? size : undefined} | ||
| {...props} | ||
| /> | ||
| )} | ||
| </span> | ||
| ); | ||
| } | ||
|
|
||
| export { Input, type InputProps }; |
There was a problem hiding this comment.
Place this Input in packages/ui and import it from there.
This component lives in apps/web, but project conventions prefer shared UI primitives. Moving it to packages/ui (or re-exporting it from there) helps avoid divergence between apps.
Based on learnings: "Use packages/ui components - import UI components from the packages/ui package".
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/input.tsx` around lines 1 - 62, Move the Input
component (Input, InputProps, and its use of InputPrimitive/nativeInput) out of
the app and into the shared UI package (packages/ui) and re-export it from that
package; update the package's public exports (the UI package index/exports) to
include Input, adjust all app imports to import Input from the shared UI package
instead of the app path, and ensure TypeScript types and any peer deps
(base-ui/react) are declared in the UI package so the component builds
correctly.
| type InputProps = Omit<InputPrimitive.Props & React.RefAttributes<HTMLInputElement>, "size"> & { | ||
| size?: "sm" | "default" | "lg" | number; | ||
| unstyled?: boolean; | ||
| nativeInput?: boolean; | ||
| }; | ||
|
|
||
| function Input({ | ||
| className, | ||
| size = "default", | ||
| unstyled = false, | ||
| nativeInput = false, | ||
| ...props | ||
| }: InputProps) { | ||
| const inputClassName = cn( | ||
| "h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none placeholder:text-muted-foreground/72 sm:h-7.5 sm:leading-7.5 [transition:background-color_5000000s_ease-in-out_0s]", | ||
| size === "sm" && "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5", | ||
| size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5", | ||
| props.type === "search" && | ||
| "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none", | ||
| props.type === "file" && | ||
| "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm", | ||
| ); | ||
|
|
||
| return ( | ||
| <span | ||
| className={ | ||
| cn( | ||
| !unstyled && | ||
| "relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]", | ||
| className, | ||
| ) || undefined | ||
| } | ||
| data-size={size} | ||
| data-slot="input-control" | ||
| > | ||
| {nativeInput ? ( | ||
| <input | ||
| className={inputClassName} | ||
| data-slot="input" | ||
| size={typeof size === "number" ? size : undefined} | ||
| {...props} | ||
| /> | ||
| ) : ( | ||
| <InputPrimitive | ||
| className={inputClassName} | ||
| data-slot="input" | ||
| size={typeof size === "number" ? size : undefined} | ||
| {...props} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat package.json | grep -A 5 '"react"'Repository: pingdotgg/codething-mvp
Length of output: 49
🏁 Script executed:
# Check if Input component is used with refs anywhere
rg -n 'ref=' apps/web/src --type=tsx | grep -i input | head -20Repository: pingdotgg/codething-mvp
Length of output: 94
🏁 Script executed:
# Check React version more thoroughly
fd package.json | xargs grep -l '"react"' | head -5 | xargs cat | grep -A 2 '"react"'Repository: pingdotgg/codething-mvp
Length of output: 151
🏁 Script executed:
fd -e ts -e tsx | xargs grep -l "InputPrimitive" | head -5Repository: pingdotgg/codething-mvp
Length of output: 103
🏁 Script executed:
# Search for Input component usage with ref in web app
rg 'Input\s*[a-zA-Z0-9]*.*ref\s*=' apps/web/src --type=ts --type=tsx 2>/dev/null | head -20Repository: pingdotgg/codething-mvp
Length of output: 49
🏁 Script executed:
# Check InputPrimitive import and definition
cat apps/web/src/components/ui/input.tsx | head -20Repository: pingdotgg/codething-mvp
Length of output: 530
Forward refs to the actual input element; current component wraps in a span.
InputProps accepts React.RefAttributes<HTMLInputElement>, but any ref passed to Input will attach to the outer <span> wrapper, not the actual input element. This breaks focus/measurement use-cases and creates a mismatch between the type signature and runtime behavior. Use React.forwardRef to explicitly wire the ref through to either the native <input> or <InputPrimitive> (depending on the nativeInput flag).
✅ Suggested fix
-import type * as React from "react";
+import * as React from "react";
-type InputProps = Omit<InputPrimitive.Props & React.RefAttributes<HTMLInputElement>, "size"> & {
+type InputProps = Omit<InputPrimitive.Props, "size"> & {
size?: "sm" | "default" | "lg" | number;
unstyled?: boolean;
nativeInput?: boolean;
};
-function Input({
+const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input({
className,
size = "default",
unstyled = false,
nativeInput = false,
...props
-}: InputProps) {
+}: InputProps, ref) {
const inputClassName = cn(
"h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none placeholder:text-muted-foreground/72 sm:h-7.5 sm:leading-7.5 [transition:background-color_5000000s_ease-in-out_0s]",
size === "sm" && "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
props.type === "search" &&
"[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
props.type === "file" &&
"text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm",
);
return (
<span
className={
cn(
!unstyled &&
"relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
) || undefined
}
data-size={size}
data-slot="input-control"
>
{nativeInput ? (
<input
className={inputClassName}
data-slot="input"
size={typeof size === "number" ? size : undefined}
+ ref={ref}
{...props}
/>
) : (
<InputPrimitive
className={inputClassName}
data-slot="input"
size={typeof size === "number" ? size : undefined}
+ ref={ref}
{...props}
/>
)}
</span>
);
-}
+});
+
+Input.displayName = "Input";🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/input.tsx` around lines 8 - 57, The Input
component declares refs for HTMLInputElement but currently wraps the input in a
span so incoming refs attach to the span; convert the Input function into a
React.forwardRef component (export default React.forwardRef or similar) that
accepts a ref parameter typed as HTMLInputElement and pass that ref through to
the actual input element (for nativeInput case) or to the underlying
InputPrimitive (for non-native case), keeping the existing size prop handling
and props spread; update InputProps to remove/react-adjust RefAttributes if
needed so the forwarded ref type matches the component signature and ensure the
span remains without receiving the forwarded ref.
| nativeInput?: boolean; | ||
| }; | ||
|
|
||
| function Input({ |
There was a problem hiding this comment.
🟡 Medium
ui/input.tsx:14 Refs won't forward to the underlying <input> because Input isn't wrapped in React.forwardRef. Consider wrapping this component with forwardRef and explicitly passing ref to the input elements.
🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/web/src/components/ui/input.tsx around line 14:
Refs won't forward to the underlying `<input>` because `Input` isn't wrapped in `React.forwardRef`. Consider wrapping this component with `forwardRef` and explicitly passing `ref` to the input elements.
| const isRepo = branchList?.isRepo ?? true; | ||
|
|
||
| const isGitActionRunning = isGitModalActionRunning || runImmediateGitActionMutation.isPending; | ||
| const initMutation = useMutation(gitInitMutationOptions({ api, cwd: gitCwd, queryClient })); |
There was a problem hiding this comment.
🟡 Medium
components/GitActionsControl.tsx:157 When gitCwd changes, activeDialogAction isn't reset, so an open dialog could execute against a different repository. The missing useEffect that was removed in this refactor should be restored to reset dialog state when gitCwd changes.
const [dialogCommitMessage, setDialogCommitMessage] = useState("");
+
+ useEffect(() => {
+ setActiveDialogAction(null);
+ setDialogCommitMessage("");
+ }, [gitCwd]);🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/web/src/components/GitActionsControl.tsx around line 157:
When `gitCwd` changes, `activeDialogAction` isn't reset, so an open dialog could execute against a different repository. The missing `useEffect` that was removed in this refactor should be restored to reset dialog state when `gitCwd` changes.
| strokeWidth="2" | ||
| viewBox="0 0 24 24" | ||
| width="24" | ||
| xmlns="http://www.w3.org/1500/svg" |
There was a problem hiding this comment.
🟢 Low
ui/select.tsx:186 Typo in SVG namespace: 1500 should be 2000. The checkmark won't render correctly in strict XML environments.
| xmlns="http://www.w3.org/1500/svg" | |
| xmlns="http://www.w3.org/2000/svg" |
🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/web/src/components/ui/select.tsx around line 186:
Typo in SVG namespace: `1500` should be `2000`. The checkmark won't render correctly in strict XML environments.
- Replace nested conditional strings with action-to-copy lookup objects - Keep commit/push/create PR dialog text centralized and easier to maintain
- let users create a new local branch directly from the branch combobox search - remove separate "New branch" form/button flow in `BranchToolbar` - add `inputClassName` support to `ComboboxInput` for native input styling control
- Standardize quick action icon sizing and label spacing in chat and git controls - Replace manual popover hover state handling with `openOnHover` triggers - Add reusable terminal action button wrapper with hover tooltips for split/new/close controls
| className={cn( | ||
| startAddon && | ||
| "data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=autocomplete-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(8)-1px)]", | ||
| sizeValue === "sm" | ||
| ? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5" | ||
| : "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7", | ||
| className, | ||
| )} |
There was a problem hiding this comment.
🟢 Low
ui/autocomplete.tsx:40 The *:data-[slot=autocomplete-input] selector targets children of elements with that attribute, but data-slot="autocomplete-input" is on the wrapper itself (not its parent). Consider changing *:data-[slot=autocomplete-input] to *:data-[slot=input] to target the actual input element inside the wrapper.
| className={cn( | |
| startAddon && | |
| "data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=autocomplete-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(8)-1px)]", | |
| sizeValue === "sm" | |
| ? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5" | |
| : "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7", | |
| className, | |
| )} | |
| className={cn( | |
| startAddon && | |
| "data-[size=sm]:*:data-[slot=input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=input]:ps-[calc(--spacing(8)-1px)]", | |
| sizeValue === "sm" | |
| ? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=input]:pe-6.5" | |
| : "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=input]:pe-7", | |
| className, | |
| )} |
🚀 Want me to fix this? Reply ex: "fix it for me".
🤖 Prompt for AI
In file apps/web/src/components/ui/autocomplete.tsx around lines 40-47:
The `*:data-[slot=autocomplete-input]` selector targets children of elements with that attribute, but `data-slot="autocomplete-input"` is on the wrapper itself (not its parent). Consider changing `*:data-[slot=autocomplete-input]` to `*:data-[slot=input]` to target the actual input element inside the wrapper.
…ker gating Port upstream commits 9bb9023..b36888e: - Handle branch selection across main and secondary worktrees (pingdotgg#44) - Preserve fork PR upstreams when preparing local and worktree threads (pingdotgg#45) Adds resolveBranchSelectionTarget for unified checkout cwd/worktree decisions, GitCore helpers for remote management (ensureRemote, fetchRemoteBranch, setBranchUpstream), GitHub CLI cross-repo PR metadata parsing, and GitManager fork head materialization with upstream tracking.
Summary by CodeRabbit
New Features
UI Improvements
Tests