diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d588c83 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Quality Checks + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.8.1 + + - name: Setup Node.js 20.12.0 + uses: actions/setup-node@v4 + with: + node-version: 20.12.0 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run linting + run: pnpm lint + + - name: Run type checking + run: pnpm typecheck + + - name: Build project + run: pnpm build diff --git a/.node-version b/.node-version index 9d11232..2b9cabc 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -24.4.1 +20.12.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 350db11..6fa2dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## 0.3.1 + +###    Features + +- Add configurable Enter key behavior for message input  -  by **nepula_h_okuyama** and **Claude** [(e37ca)](https://github.com/d-kimuson/claude-code-viewer/commit/e37ca87) + +###    Bug Fixes + +- Resolve lint and formatting errors  -  by **amay077** and **Claude** [(730d1)](https://github.com/d-kimuson/claude-code-viewer/commit/730d134) + +#####     [View changes on GitHub](https://github.com/d-kimuson/claude-code-viewer/compare/v0.3.0...0.3.1) + +## 0.3.0 + +###    Features + +- Set timeout for new-chat & resume-chat  -  by **d-kimsuon** [(d0fda)](https://github.com/d-kimuson/claude-code-viewer/commit/d0fdade) +- Add @ file completion  -  by **d-kimsuon** [(60aaa)](https://github.com/d-kimuson/claude-code-viewer/commit/60aaae7) +- Inline completion for command and files  -  by **d-kimsuon** [(e90dc)](https://github.com/d-kimuson/claude-code-viewer/commit/e90dc00) +- Fix out of style  -  by **d-kimsuon** [(7fafb)](https://github.com/d-kimuson/claude-code-viewer/commit/7fafb18) +- Add simple git diff preview modal  -  by **d-kimsuon** [(c5688)](https://github.com/d-kimuson/claude-code-viewer/commit/c568831) +- Add comprehensive CI workflow for quality checks  -  by **d-kimsuon** and **Claude** [(580e5)](https://github.com/d-kimuson/claude-code-viewer/commit/580e51f) +- Add notification when task paused  -  by **d-kimsuon** [(8b6b0)](https://github.com/d-kimuson/claude-code-viewer/commit/8b6b03b) +- Add sonner message on task completed  -  by **d-kimsuon** [(a3e6f)](https://github.com/d-kimuson/claude-code-viewer/commit/a3e6feb) +- **diff-view**: Display untacked added file  -  by **d-kimsuon** [(e7c3c)](https://github.com/d-kimuson/claude-code-viewer/commit/e7c3c87) + +###    Bug Fixes + +- Bug fix session list doesn't updated after filter config changed  -  by **d-kimsuon** [(52a23)](https://github.com/d-kimuson/claude-code-viewer/commit/52a231b) +- Fix header text content overflow  -  by **d-kimsuon** [(a618e)](https://github.com/d-kimuson/claude-code-viewer/commit/a618e24) +- Bug fix that input message gone out though new chat is not sent yet  -  by **d-kimsuon** [(ca316)](https://github.com/d-kimuson/claude-code-viewer/commit/ca31602) +- Add unsupported container property to schema  -  by **d-kimsuon** [(c7a1e)](https://github.com/d-kimuson/claude-code-viewer/commit/c7a1e6d) + +#####     [View changes on GitHub](https://github.com/d-kimuson/claude-code-viewer/compare/v0.2.4...0.3.0) + ## 0.2.4 ###    Features diff --git a/package.json b/package.json index 6ffb430..f3fa94d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kimuson/claude-code-viewer", - "version": "0.2.4", + "version": "0.3.1", "type": "module", "license": "MIT", "repository": { @@ -44,6 +44,7 @@ "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-query": "^5.85.5", @@ -53,6 +54,8 @@ "jotai": "^2.13.1", "lucide-react": "^0.542.0", "next": "15.5.2", + "next-themes": "^0.4.6", + "parse-git-diff": "^0.0.19", "prexit": "^2.3.0", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -60,6 +63,7 @@ "react-markdown": "^10.1.0", "react-syntax-highlighter": "^15.6.6", "remark-gfm": "^4.0.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "ulid": "^3.0.1", "zod": "^4.1.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6badbfa..8ba52a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@radix-ui/react-hover-card': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.12)(react@19.1.1) @@ -56,6 +59,12 @@ importers: next: specifier: 15.5.2 version: 15.5.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + parse-git-diff: + specifier: ^0.0.19 + version: 0.0.19 prexit: specifier: ^2.3.0 version: 2.3.0 @@ -77,6 +86,9 @@ importers: remark-gfm: specifier: ^4.0.1 version: 4.0.1 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -842,6 +854,9 @@ packages: resolution: {integrity: sha512-7J6ca1tK0duM2BgVB+CuFMh3idlIVASOP2QvOCbNWDc6JnvjtKa9nufPoJQQ4xrwBonwgT1TIhRRcEtzdVgWsA==} engines: {node: ^20.9.0 || >=22.0.0, npm: '>=10.8.2'} + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -1072,6 +1087,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -1175,6 +1203,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} @@ -2517,6 +2558,12 @@ packages: resolution: {integrity: sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next@15.5.2: resolution: {integrity: sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -2618,6 +2665,9 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-git-diff@0.0.19: + resolution: {integrity: sha512-oh3giwKzsPlOhekiDDyd/pfFKn04IZoTjEThquhfKigwiUHymiP/Tp6AN5nGIwXQdWuBTQvz9AaRdN5TBsJ8MA==} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -2883,6 +2933,12 @@ packages: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3800,6 +3856,8 @@ snapshots: '@phun-ky/typeof@1.2.8': {} + '@radix-ui/number@1.1.1': {} + '@radix-ui/primitive@1.1.3': {} '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': @@ -4026,6 +4084,35 @@ snapshots: '@types/react': 19.1.12 '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@radix-ui/react-select@2.2.6(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + aria-hidden: 1.2.6 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-remove-scroll: 2.7.1(@types/react@19.1.12)(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@radix-ui/react-slot@1.2.3(@types/react@19.1.12)(react@19.1.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) @@ -4110,6 +4197,15 @@ snapshots: optionalDependencies: '@types/react': 19.1.12 + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.12 + '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@radix-ui/rect@1.1.1': {} '@rollup/rollup-android-arm-eabi@4.49.0': @@ -5567,6 +5663,11 @@ snapshots: dependencies: type-fest: 2.19.0 + next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + next@15.5.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: '@next/env': 15.5.2 @@ -5714,6 +5815,8 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-git-diff@0.0.19: {} + parse-ms@4.0.0: {} parse-path@7.1.0: @@ -6078,6 +6181,11 @@ snapshots: ip-address: 10.0.1 smart-buffer: 4.2.0 + sonner@2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + source-map-js@1.2.1: {} source-map-support@0.5.21: diff --git a/src/app/components/MarkdownContent.tsx b/src/app/components/MarkdownContent.tsx index f1c51c6..e5acb61 100644 --- a/src/app/components/MarkdownContent.tsx +++ b/src/app/components/MarkdownContent.tsx @@ -84,7 +84,10 @@ export const MarkdownContent: FC = ({ }, p({ children, ...props }) { return ( -

+

{children}

); @@ -117,7 +120,7 @@ export const MarkdownContent: FC = ({ if (isInline) { return ( {children} @@ -175,8 +178,8 @@ export const MarkdownContent: FC = ({ // テーブルの改善 table({ children, ...props }) { return ( -
- +
+
{children}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9b74311..0aa6bf9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { Toaster } from "../components/ui/sonner"; import { QueryClientProviderWrapper } from "../lib/api/QueryClientProviderWrapper"; import { RootErrorBoundary } from "./components/RootErrorBoundary"; import { ServerEventsProvider } from "./components/ServerEventsProvider"; @@ -47,6 +48,7 @@ export default async function RootLayout({ {children} + ); diff --git a/src/app/projects/[projectId]/components/ProjectPage.tsx b/src/app/projects/[projectId]/components/ProjectPage.tsx index 3cfa5ac..49619b5 100644 --- a/src/app/projects/[projectId]/components/ProjectPage.tsx +++ b/src/app/projects/[projectId]/components/ProjectPage.tsx @@ -45,12 +45,6 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => { }); }, [config.hideNoUserMessageSession, config.unifySameTitleSession]); - const handleConfigChange = () => { - void queryClient.invalidateQueries({ - queryKey: projectQueryConfig(projectId).queryKey, - }); - }; - return (
@@ -118,7 +112,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
- +
diff --git a/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx b/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx new file mode 100644 index 0000000..e707fd4 --- /dev/null +++ b/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx @@ -0,0 +1,226 @@ +import { AlertCircleIcon, LoaderIcon, SendIcon } from "lucide-react"; +import { type FC, useCallback, useId, useRef, useState } from "react"; +import { Button } from "../../../../../components/ui/button"; +import { Textarea } from "../../../../../components/ui/textarea"; +import { useConfig } from "../../../../hooks/useConfig"; +import type { CommandCompletionRef } from "./CommandCompletion"; +import type { FileCompletionRef } from "./FileCompletion"; +import { InlineCompletion } from "./InlineCompletion"; + +export interface ChatInputProps { + projectId: string; + onSubmit: (message: string) => Promise; + isPending: boolean; + error?: Error | null; + placeholder: string; + buttonText: string; + minHeight?: string; + containerClassName?: string; + disabled?: boolean; + buttonSize?: "sm" | "default" | "lg"; +} + +export const ChatInput: FC = ({ + projectId, + onSubmit, + isPending, + error, + placeholder, + buttonText, + minHeight = "min-h-[100px]", + containerClassName = "", + disabled = false, + buttonSize = "lg", +}) => { + const [message, setMessage] = useState(""); + const [cursorPosition, setCursorPosition] = useState<{ + relative: { top: number; left: number }; + absolute: { top: number; left: number }; + }>({ relative: { top: 0, left: 0 }, absolute: { top: 0, left: 0 } }); + + const containerRef = useRef(null); + const textareaRef = useRef(null); + const commandCompletionRef = useRef(null); + const fileCompletionRef = useRef(null); + const helpId = useId(); + const { config } = useConfig(); + + const handleSubmit = async () => { + if (!message.trim()) return; + await onSubmit(message.trim()); + setMessage(""); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (fileCompletionRef.current?.handleKeyDown(e)) { + return; + } + + if (commandCompletionRef.current?.handleKeyDown(e)) { + return; + } + + // IMEで変換中の場合は送信しない + if (e.key === "Enter" && !e.nativeEvent.isComposing) { + const isEnterSend = config?.enterKeyBehavior === "enter-send"; + + if (isEnterSend && !e.shiftKey) { + // Enter: Send mode + e.preventDefault(); + handleSubmit(); + } else if (!isEnterSend && e.shiftKey) { + // Shift+Enter: Send mode (default) + e.preventDefault(); + handleSubmit(); + } + } + }; + + const getCursorPosition = useCallback(() => { + const textarea = textareaRef.current; + const container = containerRef.current; + if (textarea === null || container === null) return undefined; + + const cursorPos = textarea.selectionStart; + const textBeforeCursor = textarea.value.substring(0, cursorPos); + const textAfterCursor = textarea.value.substring(cursorPos); + + const pre = document.createTextNode(textBeforeCursor); + const post = document.createTextNode(textAfterCursor); + const caret = document.createElement("span"); + caret.innerHTML = " "; + + const mirrored = document.createElement("div"); + + mirrored.innerHTML = ""; + mirrored.append(pre, caret, post); + + const textareaStyles = window.getComputedStyle(textarea); + for (const property of [ + "border", + "boxSizing", + "fontFamily", + "fontSize", + "fontWeight", + "letterSpacing", + "lineHeight", + "padding", + "textDecoration", + "textIndent", + "textTransform", + "whiteSpace", + "wordSpacing", + "wordWrap", + ] as const) { + mirrored.style[property] = textareaStyles[property]; + } + + mirrored.style.visibility = "hidden"; + container.prepend(mirrored); + + const caretRect = caret.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + container.removeChild(mirrored); + + return { + relative: { + top: caretRect.top - containerRect.top - textarea.scrollTop, + left: caretRect.left - containerRect.left - textarea.scrollLeft, + }, + absolute: { + top: caretRect.top - textarea.scrollTop, + left: caretRect.left - textarea.scrollLeft, + }, + }; + }, []); + + const handleCommandSelect = (command: string) => { + setMessage(command); + textareaRef.current?.focus(); + }; + + const handleFileSelect = (filePath: string) => { + setMessage(filePath); + textareaRef.current?.focus(); + }; + + return ( +
+ {error && ( +
+ + Failed to send message. Please try again. +
+ )} + +
+
+