chore: Improve session detail UI (#49)
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 456 KiB After Width: | Height: | Size: 416 KiB |
|
Before Width: | Height: | Size: 458 KiB After Width: | Height: | Size: 326 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 362 KiB After Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 371 KiB After Width: | Height: | Size: 318 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 326 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 326 KiB |
|
Before Width: | Height: | Size: 372 KiB After Width: | Height: | Size: 326 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 83 KiB |
@@ -37,6 +37,7 @@
|
||||
"e2e": "./scripts/e2e/exec_e2e.sh",
|
||||
"e2e:start-server": "./scripts/e2e/start_server.sh",
|
||||
"e2e:capture-snapshots": "./scripts/e2e/capture_snapshots.sh",
|
||||
"lingui:extract": "lingui extract --clean",
|
||||
"lingui:compile": "lingui compile --typescript"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -54,6 +55,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-popover": "^1.1.15",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
|
||||
39
pnpm-lock.yaml
generated
@@ -50,6 +50,9 @@ importers:
|
||||
'@radix-ui/react-hover-card':
|
||||
specifier: 1.1.15
|
||||
version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-popover':
|
||||
specifier: ^1.1.15
|
||||
version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-select':
|
||||
specifier: 2.2.6
|
||||
version: 2.2.6(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -1339,6 +1342,19 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-popover@1.1.15':
|
||||
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
|
||||
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-popper@1.2.8':
|
||||
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
|
||||
peerDependencies:
|
||||
@@ -5573,6 +5589,29 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
|
||||
'@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0)
|
||||
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0)
|
||||
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0)
|
||||
aria-hidden: 1.2.6
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
'@types/react-dom': 19.2.2(@types/react@19.2.2)
|
||||
|
||||
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
|
||||
@@ -7,7 +7,14 @@ import {
|
||||
SparklesIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { type FC, useCallback, useId, useRef, useState } from "react";
|
||||
import {
|
||||
type FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../../../../components/ui/button";
|
||||
import { Input } from "../../../../../components/ui/input";
|
||||
@@ -59,13 +66,28 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
error,
|
||||
placeholder,
|
||||
buttonText,
|
||||
minHeight = "min-h-[100px]",
|
||||
minHeight: minHeightProp = "min-h-[64px]",
|
||||
containerClassName = "",
|
||||
disabled = false,
|
||||
buttonSize = "lg",
|
||||
enableScheduledSend = false,
|
||||
baseSessionId = null,
|
||||
}) => {
|
||||
// Parse minHeight prop to get pixel value (default to 48px for 1.5 lines)
|
||||
// Supports both "200px" and Tailwind format like "min-h-[200px]"
|
||||
const parseMinHeight = (value: string): number => {
|
||||
// Try to extract pixel value using regex (handles both formats)
|
||||
const match = value.match(/(\d+)px/);
|
||||
if (match?.[1]) {
|
||||
const parsed = parseInt(match[1], 10);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
// Fallback to default
|
||||
return 48;
|
||||
};
|
||||
const minHeightValue = parseMinHeight(minHeightProp);
|
||||
const { i18n } = useLingui();
|
||||
const [message, setMessage] = useState("");
|
||||
const [attachedFiles, setAttachedFiles] = useState<
|
||||
@@ -98,6 +120,28 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
const { config } = useConfig();
|
||||
const createSchedulerJob = useCreateSchedulerJob();
|
||||
|
||||
// Auto-resize textarea based on content
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: message is intentionally included to trigger resize
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
// Reset height to auto to get the correct scrollHeight
|
||||
textarea.style.height = "auto";
|
||||
// Set height to scrollHeight, but respect min/max constraints
|
||||
const scrollHeight = textarea.scrollHeight;
|
||||
const maxHeight = 200; // Maximum height in pixels (approx 5 lines)
|
||||
textarea.style.height = `${Math.max(minHeightValue, Math.min(scrollHeight, maxHeight))}px`;
|
||||
}, [message, minHeightValue]);
|
||||
|
||||
// Set initial height to 1 line on mount
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
// Set initial height to minHeight value
|
||||
textarea.style.height = `${minHeightValue}px`;
|
||||
}, [minHeightValue]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!message.trim() && attachedFiles.length === 0) return;
|
||||
|
||||
@@ -328,10 +372,7 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
<div className="flex items-center gap-2.5 px-4 py-3 text-sm text-red-600 dark:text-red-400 bg-gradient-to-r from-red-50 to-red-100/50 dark:from-red-950/30 dark:to-red-900/20 border border-red-200/50 dark:border-red-800/50 rounded-xl mb-4 animate-in fade-in slide-in-from-top-2 duration-300 shadow-sm">
|
||||
<AlertCircleIcon className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
<span className="font-medium">
|
||||
<Trans
|
||||
id="chat.error.send_failed"
|
||||
message="Failed to send message. Please try again."
|
||||
/>
|
||||
<Trans id="chat.error.send_failed" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -362,7 +403,10 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className={`${minHeight} resize-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 bg-transparent px-5 py-4 text-lg transition-all duration-200 placeholder:text-muted-foreground/60`}
|
||||
className="resize-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 bg-transparent px-5 py-2 text-base transition-all duration-200 placeholder:text-muted-foreground/60 overflow-y-auto leading-6"
|
||||
style={{
|
||||
minHeight: `${minHeightValue}px`,
|
||||
}}
|
||||
disabled={isPending || disabled}
|
||||
aria-label={i18n._("Message input with completion support")}
|
||||
aria-describedby={helpId}
|
||||
@@ -394,7 +438,7 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-3 px-5 py-3 bg-muted/30 border-t border-border/40">
|
||||
<div className="flex items-center justify-between gap-3 px-5 py-1 bg-muted/30 border-t border-border/40">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
@@ -413,7 +457,7 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
>
|
||||
<PaperclipIcon className="w-4 h-4" />
|
||||
<span className="text-xs">
|
||||
<Trans id="chat.attach_file" message="Attach" />
|
||||
<Trans id="chat.attach_file" />
|
||||
</span>
|
||||
</Button>
|
||||
<span
|
||||
@@ -425,10 +469,7 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
{(message.startsWith("/") || message.includes("@")) && (
|
||||
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium flex items-center gap-1">
|
||||
<SparklesIcon className="w-3 h-3" />
|
||||
<Trans
|
||||
id="chat.autocomplete.active"
|
||||
message="Autocomplete active"
|
||||
/>
|
||||
<Trans id="chat.autocomplete.active" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -437,7 +478,7 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
{enableScheduledSend && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="send-mode" className="text-xs sr-only">
|
||||
<Trans id="chat.send_mode.label" message="Send mode" />
|
||||
<Trans id="chat.send_mode.label" />
|
||||
</Label>
|
||||
<Select
|
||||
value={sendMode}
|
||||
@@ -454,16 +495,10 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="immediate">
|
||||
<Trans
|
||||
id="chat.send_mode.immediate"
|
||||
message="Send now"
|
||||
/>
|
||||
<Trans id="chat.send_mode.immediate" />
|
||||
</SelectItem>
|
||||
<SelectItem value="scheduled">
|
||||
<Trans
|
||||
id="chat.send_mode.scheduled"
|
||||
message="Schedule send"
|
||||
/>
|
||||
<Trans id="chat.send_mode.scheduled" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -474,10 +509,7 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
htmlFor="scheduled-time"
|
||||
className="text-xs sr-only"
|
||||
>
|
||||
<Trans
|
||||
id="chat.send_mode.scheduled_time"
|
||||
message="Scheduled time"
|
||||
/>
|
||||
<Trans id="chat.send_mode.scheduled_time" />
|
||||
</Label>
|
||||
<Input
|
||||
id="scheduled-time"
|
||||
@@ -506,10 +538,7 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
<>
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
<span>
|
||||
<Trans
|
||||
id="chat.status.processing"
|
||||
message="Processing..."
|
||||
/>
|
||||
<Trans id="chat.status.processing" />
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -107,7 +107,7 @@ export const InlineCompletion: FC<{
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute w-full max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl"
|
||||
className="absolute w-full max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl z-50"
|
||||
style={{
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
|
||||
@@ -46,7 +46,7 @@ export const NewChat: FC<{
|
||||
isPending={createSessionProcess.isPending}
|
||||
error={createSessionProcess.error}
|
||||
placeholder={getPlaceholder()}
|
||||
buttonText={<Trans id="chat.button.start" message="Start Chat" />}
|
||||
buttonText={<Trans id="chat.button.start" />}
|
||||
minHeight="min-h-[200px]"
|
||||
containerClassName="px-0 py-6"
|
||||
buttonSize="lg"
|
||||
|
||||
@@ -30,7 +30,7 @@ export const NewChatModal: FC<{
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<MessageSquareIcon className="w-5 h-5" />
|
||||
<Trans id="chat.modal.title" message="Start New Chat" />
|
||||
<Trans id="chat.modal.title" />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<NewChat projectId={projectId} onSuccess={handleSuccess} />
|
||||
|
||||
@@ -2,27 +2,24 @@ import type { FC } from "react";
|
||||
import { Suspense, useState } from "react";
|
||||
|
||||
import { Loading } from "@/components/Loading";
|
||||
import { SessionPageMain } from "./SessionPageMain";
|
||||
import { SessionSidebar } from "./sessionSidebar/SessionSidebar";
|
||||
import { SessionPageMainWrapper } from "./SessionPageMainWrapper";
|
||||
import type { Tab } from "./sessionSidebar/schema";
|
||||
|
||||
export const SessionPageContent: FC<{
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
}> = ({ projectId, sessionId }) => {
|
||||
tab: Tab;
|
||||
}> = ({ projectId, sessionId, tab }) => {
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen max-h-screen overflow-hidden">
|
||||
<SessionSidebar
|
||||
currentSessionId={sessionId}
|
||||
projectId={projectId}
|
||||
isMobileOpen={isMobileSidebarOpen}
|
||||
onMobileOpenChange={setIsMobileSidebarOpen}
|
||||
/>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SessionPageMain
|
||||
<SessionPageMainWrapper
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
tab={tab}
|
||||
isMobileSidebarOpen={isMobileSidebarOpen}
|
||||
setIsMobileSidebarOpen={setIsMobileSidebarOpen}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
import { Trans } from "@lingui/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import {
|
||||
GitBranchIcon,
|
||||
GitCompareIcon,
|
||||
InfoIcon,
|
||||
LoaderIcon,
|
||||
MenuIcon,
|
||||
PauseIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { type FC, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { type FC, type RefObject, useEffect, useMemo, useState } from "react";
|
||||
import { PermissionDialog } from "@/components/PermissionDialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { usePermissionRequests } from "@/hooks/usePermissionRequests";
|
||||
import { useTaskNotifications } from "@/hooks/useTaskNotifications";
|
||||
import { Badge } from "../../../../../../components/ui/badge";
|
||||
import { honoClient } from "../../../../../../lib/api/client";
|
||||
import { useProject } from "../../../hooks/useProject";
|
||||
import type { PublicSessionProcess } from "@/types/session-process";
|
||||
import { firstUserMessageToTitle } from "../../../services/firstCommandToTitle";
|
||||
import { useGitCurrentRevisions } from "../hooks/useGit";
|
||||
import type { useGitCurrentRevisions } from "../hooks/useGit";
|
||||
import { useGitCurrentRevisions as useGitCurrentRevisionsHook } from "../hooks/useGit";
|
||||
import { useSession } from "../hooks/useSession";
|
||||
import { useSessionProcess } from "../hooks/useSessionProcess";
|
||||
import { ConversationList } from "./conversationList/ConversationList";
|
||||
import { DiffModal } from "./diffModal";
|
||||
import { ChatActionMenu } from "./resumeChat/ChatActionMenu";
|
||||
import { ContinueChat } from "./resumeChat/ContinueChat";
|
||||
import { ResumeChat } from "./resumeChat/ResumeChat";
|
||||
|
||||
@@ -30,35 +39,42 @@ export const SessionPageMain: FC<{
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
setIsMobileSidebarOpen: (open: boolean) => void;
|
||||
}> = ({ projectId, sessionId, setIsMobileSidebarOpen }) => {
|
||||
isDiffModalOpen: boolean;
|
||||
setIsDiffModalOpen: (open: boolean) => void;
|
||||
scrollContainerRef: RefObject<HTMLDivElement | null>;
|
||||
onScrollToTop?: () => void;
|
||||
onScrollToBottom?: () => void;
|
||||
onOpenDiffModal?: () => void;
|
||||
abortTask?: UseMutationResult<unknown, Error, string, unknown>;
|
||||
projectPath?: string;
|
||||
currentBranch?: string;
|
||||
sessionProcessStatus?: PublicSessionProcess["status"];
|
||||
revisionsData?: ReturnType<typeof useGitCurrentRevisions>["data"];
|
||||
}> = ({
|
||||
projectId,
|
||||
sessionId,
|
||||
setIsMobileSidebarOpen,
|
||||
isDiffModalOpen,
|
||||
setIsDiffModalOpen,
|
||||
scrollContainerRef,
|
||||
onScrollToTop,
|
||||
onScrollToBottom,
|
||||
onOpenDiffModal,
|
||||
abortTask,
|
||||
projectPath,
|
||||
currentBranch,
|
||||
sessionProcessStatus,
|
||||
revisionsData: revisionsDataProp,
|
||||
}) => {
|
||||
const { session, conversations, getToolResult } = useSession(
|
||||
projectId,
|
||||
sessionId,
|
||||
);
|
||||
const { data: projectData } = useProject(projectId);
|
||||
// biome-ignore lint/style/noNonNullAssertion: useSuspenseInfiniteQuery guarantees at least one page
|
||||
const project = projectData.pages[0]!.project;
|
||||
const { currentPermissionRequest, isDialogOpen, onPermissionResponse } =
|
||||
usePermissionRequests();
|
||||
const [isDiffModalOpen, setIsDiffModalOpen] = useState(false);
|
||||
const { data: revisionsData } = useGitCurrentRevisions(projectId);
|
||||
const { data: revisionsDataFallback } = useGitCurrentRevisionsHook(projectId);
|
||||
const revisionsData = revisionsDataProp ?? revisionsDataFallback;
|
||||
|
||||
const abortTask = useMutation({
|
||||
mutationFn: async (sessionProcessId: string) => {
|
||||
const response = await honoClient.api.cc["session-processes"][
|
||||
":sessionProcessId"
|
||||
].abort.$post({
|
||||
param: { sessionProcessId },
|
||||
json: { projectId },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
const sessionProcess = useSessionProcess();
|
||||
|
||||
const relatedSessionProcess = useMemo(
|
||||
@@ -71,7 +87,6 @@ export const SessionPageMain: FC<{
|
||||
|
||||
const [previousConversationLength, setPreviousConversationLength] =
|
||||
useState(0);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 自動スクロール処理
|
||||
useEffect(() => {
|
||||
@@ -92,124 +107,162 @@ export const SessionPageMain: FC<{
|
||||
conversations,
|
||||
relatedSessionProcess?.status,
|
||||
previousConversationLength,
|
||||
scrollContainerRef.current,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 flex flex-col min-h-0 min-w-0">
|
||||
<header className="px-2 sm:px-3 py-2 sm:py-3 sticky top-0 z-10 bg-background w-full flex-shrink-0 min-w-0">
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="md:hidden flex-shrink-0"
|
||||
onClick={() => setIsMobileSidebarOpen(true)}
|
||||
data-testid="mobile-sidebar-toggle-button"
|
||||
>
|
||||
<MenuIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<h1 className="text-lg sm:text-2xl md:text-3xl font-bold break-all overflow-ellipsis line-clamp-1 px-1 sm:px-5 min-w-0">
|
||||
{session.meta.firstUserMessage !== null
|
||||
? firstUserMessageToTitle(session.meta.firstUserMessage)
|
||||
: sessionId}
|
||||
</h1>
|
||||
<header className="px-2 sm:px-3 py-2 sm:py-3 sticky top-0 z-10 bg-background w-full flex-shrink-0 min-w-0 border-b space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="md:hidden flex-shrink-0"
|
||||
onClick={() => setIsMobileSidebarOpen(true)}
|
||||
data-testid="mobile-sidebar-toggle-button"
|
||||
>
|
||||
<MenuIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<h1 className="text-lg sm:text-2xl md:text-3xl font-bold break-all overflow-ellipsis line-clamp-1 min-w-0">
|
||||
{session.meta.firstUserMessage !== null
|
||||
? firstUserMessageToTitle(session.meta.firstUserMessage)
|
||||
: sessionId}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="flex items-center gap-1.5 min-w-0 overflow-hidden flex-1">
|
||||
{projectPath && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 text-xs flex items-center max-w-full cursor-help"
|
||||
>
|
||||
<span className="truncate">
|
||||
{projectPath.split("/").pop()}
|
||||
</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{projectPath}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentBranch && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 text-xs flex items-center gap-1 max-w-full cursor-help"
|
||||
>
|
||||
<GitBranchIcon className="w-3 h-3 flex-shrink-0" />
|
||||
<span className="truncate">{currentBranch}</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans id="control.branch" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{sessionId && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 text-xs flex items-center max-w-full font-mono cursor-help"
|
||||
>
|
||||
<span className="truncate">{sessionId}</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans id="control.session_id" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-1 sm:px-5 flex flex-wrap items-center gap-1 sm:gap-2">
|
||||
{project?.claudeProjectPath && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 sm:h-8 text-xs sm:text-sm flex items-center"
|
||||
>
|
||||
{project.meta.projectPath ?? project.claudeProjectPath}
|
||||
</Badge>
|
||||
)}
|
||||
{revisionsData?.success && revisionsData.data.currentBranch && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 sm:h-8 text-xs sm:text-sm flex items-center gap-1"
|
||||
>
|
||||
<GitBranchIcon className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
|
||||
{revisionsData.data.currentBranch.name}
|
||||
</Badge>
|
||||
)}
|
||||
{sessionProcessStatus === "running" && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 sm:h-8 text-xs sm:text-sm flex items-center"
|
||||
className="bg-green-500/10 text-green-900 dark:text-green-200 border-green-500/20 flex-shrink-0 h-6 text-xs"
|
||||
>
|
||||
{sessionId}
|
||||
<LoaderIcon className="w-3 h-3 mr-1 animate-spin" />
|
||||
<Trans id="session.conversation.running" />
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{relatedSessionProcess?.status === "running" && (
|
||||
<div className="flex items-center gap-1 sm:gap-2 p-1 bg-primary/10 border border-primary/20 rounded-lg mx-1 sm:mx-5 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<LoaderIcon className="w-3 h-3 sm:w-4 sm:h-4 animate-spin text-primary" />
|
||||
<div className="flex-1">
|
||||
<p className="text-xs sm:text-sm font-medium">
|
||||
<Trans
|
||||
id="session.conversation.in.progress"
|
||||
message="Conversation is in progress..."
|
||||
/>
|
||||
</p>
|
||||
<div className="w-full bg-primary/10 rounded-full h-1 mt-1 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary rounded-full animate-pulse"
|
||||
style={{ width: "70%" }}
|
||||
/>
|
||||
)}
|
||||
{sessionProcessStatus === "paused" && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-orange-500/10 text-orange-900 dark:text-orange-200 border-orange-500/20 flex-shrink-0 h-6 text-xs"
|
||||
>
|
||||
<PauseIcon className="w-3 h-3 mr-1" />
|
||||
<Trans id="session.conversation.paused" />
|
||||
</Badge>
|
||||
)}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="flex-shrink-0 h-6 w-6"
|
||||
aria-label="Session metadata"
|
||||
>
|
||||
<InfoIcon className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80" align="end">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm mb-2">
|
||||
<Trans id="control.metadata" />
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{projectPath && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Trans id="control.project_path" />
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-7 text-xs flex items-center w-fit cursor-help"
|
||||
>
|
||||
{projectPath.split("/").pop()}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{projectPath}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Trans id="control.session_id" />
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-7 text-xs flex items-center w-fit font-mono"
|
||||
>
|
||||
{sessionId}
|
||||
</Badge>
|
||||
</div>
|
||||
{currentBranch && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Trans id="control.branch" />
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-7 text-xs flex items-center gap-1 w-fit"
|
||||
>
|
||||
<GitBranchIcon className="w-3 h-3" />
|
||||
{currentBranch}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
abortTask.mutate(relatedSessionProcess.id);
|
||||
}}
|
||||
disabled={abortTask.isPending}
|
||||
>
|
||||
{abortTask.isPending ? (
|
||||
<LoaderIcon className="w-3 h-3 sm:w-4 sm:h-4 animate-spin" />
|
||||
) : (
|
||||
<XIcon className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
)}
|
||||
<span className="hidden sm:inline">
|
||||
<Trans id="session.conversation.abort" message="Abort" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{relatedSessionProcess?.status === "paused" && (
|
||||
<div className="flex items-center gap-1 sm:gap-2 p-1 bg-orange-50/80 dark:bg-orange-950/50 border border-orange-300/50 dark:border-orange-800/50 rounded-lg mx-1 sm:mx-5 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<PauseIcon className="w-3 h-3 sm:w-4 sm:h-4 text-orange-600 dark:text-orange-400 animate-pulse" />
|
||||
<div className="flex-1">
|
||||
<p className="text-xs sm:text-sm font-medium text-orange-900 dark:text-orange-200">
|
||||
<Trans
|
||||
id="session.conversation.paused"
|
||||
message="Conversation is paused..."
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
abortTask.mutate(relatedSessionProcess.id);
|
||||
}}
|
||||
disabled={abortTask.isPending}
|
||||
className="hover:bg-orange-100 dark:hover:bg-orange-900/50 text-orange-900 dark:text-orange-200"
|
||||
>
|
||||
{abortTask.isPending ? (
|
||||
<LoaderIcon className="w-3 h-3 sm:w-4 sm:h-4 animate-spin" />
|
||||
) : (
|
||||
<XIcon className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
)}
|
||||
<span className="hidden sm:inline">
|
||||
<Trans id="session.conversation.abort" message="Abort" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -218,7 +271,7 @@ export const SessionPageMain: FC<{
|
||||
className="flex-1 overflow-y-auto min-h-0 min-w-0"
|
||||
data-testid="scrollable-content"
|
||||
>
|
||||
<main className="w-full px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 relative z-5 min-w-0">
|
||||
<main className="w-full px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 relative min-w-0 pb-4">
|
||||
<ConversationList
|
||||
conversations={conversations}
|
||||
getToolResult={getToolResult}
|
||||
@@ -232,36 +285,39 @@ export const SessionPageMain: FC<{
|
||||
<div className="absolute inset-0 rounded-full bg-primary/20 animate-ping" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground font-medium animate-pulse">
|
||||
<Trans
|
||||
id="session.processing"
|
||||
message="Claude Code is processing..."
|
||||
/>
|
||||
<Trans id="session.processing" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{relatedSessionProcess !== undefined ? (
|
||||
<ContinueChat
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
sessionProcessId={relatedSessionProcess.id}
|
||||
/>
|
||||
) : (
|
||||
<ResumeChat projectId={projectId} sessionId={sessionId} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed Diff Button */}
|
||||
<Button
|
||||
onClick={() => setIsDiffModalOpen(true)}
|
||||
className="fixed bottom-15 right-6 w-14 h-14 rounded-full shadow-lg hover:shadow-xl transition-all duration-200 z-50"
|
||||
size="lg"
|
||||
>
|
||||
<GitCompareIcon className="w-6 h-6" />
|
||||
</Button>
|
||||
<div className="w-full pt-3">
|
||||
<ChatActionMenu
|
||||
projectId={projectId}
|
||||
onScrollToTop={onScrollToTop}
|
||||
onScrollToBottom={onScrollToBottom}
|
||||
onOpenDiffModal={onOpenDiffModal}
|
||||
sessionProcess={relatedSessionProcess}
|
||||
abortTask={abortTask}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Fixed Chat Form */}
|
||||
<div className="flex-shrink-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
{relatedSessionProcess !== undefined ? (
|
||||
<ContinueChat
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
sessionProcessId={relatedSessionProcess.id}
|
||||
sessionProcessStatus={relatedSessionProcess.status}
|
||||
/>
|
||||
) : (
|
||||
<ResumeChat projectId={projectId} sessionId={sessionId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diff Modal */}
|
||||
<DiffModal
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { FC } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { honoClient } from "@/lib/api/client";
|
||||
import { useProject } from "../../../hooks/useProject";
|
||||
import { useGitCurrentRevisions } from "../hooks/useGit";
|
||||
import { useSession } from "../hooks/useSession";
|
||||
import { useSessionProcess } from "../hooks/useSessionProcess";
|
||||
import { SessionPageMain } from "./SessionPageMain";
|
||||
import { SessionSidebar } from "./sessionSidebar/SessionSidebar";
|
||||
import type { Tab } from "./sessionSidebar/schema";
|
||||
|
||||
export const SessionPageMainWrapper: FC<{
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
isMobileSidebarOpen: boolean;
|
||||
setIsMobileSidebarOpen: (open: boolean) => void;
|
||||
tab: Tab;
|
||||
}> = ({
|
||||
projectId,
|
||||
sessionId,
|
||||
isMobileSidebarOpen,
|
||||
setIsMobileSidebarOpen,
|
||||
tab,
|
||||
}) => {
|
||||
useSession(projectId, sessionId);
|
||||
const { data: projectData } = useProject(projectId);
|
||||
// biome-ignore lint/style/noNonNullAssertion: useSuspenseInfiniteQuery guarantees at least one page
|
||||
const project = projectData.pages[0]!.project;
|
||||
const { data: revisionsData } = useGitCurrentRevisions(projectId);
|
||||
const [isDiffModalOpen, setIsDiffModalOpen] = useState(false);
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const abortTask = useMutation({
|
||||
mutationFn: async (sessionProcessId: string) => {
|
||||
const response = await honoClient.api.cc["session-processes"][
|
||||
":sessionProcessId"
|
||||
].abort.$post({
|
||||
param: { sessionProcessId },
|
||||
json: { projectId },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
const sessionProcess = useSessionProcess();
|
||||
const relatedSessionProcess = useMemo(
|
||||
() => sessionProcess.getSessionProcess(sessionId),
|
||||
[sessionProcess, sessionId],
|
||||
);
|
||||
|
||||
const handleScrollToTop = () => {
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollToBottom = () => {
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: scrollContainer.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const projectPath = project.meta.projectPath ?? project.claudeProjectPath;
|
||||
const currentBranch = revisionsData?.success
|
||||
? revisionsData.data.currentBranch?.name
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SessionSidebar
|
||||
currentSessionId={sessionId}
|
||||
projectId={projectId}
|
||||
isMobileOpen={isMobileSidebarOpen}
|
||||
onMobileOpenChange={setIsMobileSidebarOpen}
|
||||
initialTab={tab}
|
||||
/>
|
||||
<SessionPageMain
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
setIsMobileSidebarOpen={setIsMobileSidebarOpen}
|
||||
isDiffModalOpen={isDiffModalOpen}
|
||||
setIsDiffModalOpen={setIsDiffModalOpen}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
onScrollToTop={handleScrollToTop}
|
||||
onScrollToBottom={handleScrollToBottom}
|
||||
onOpenDiffModal={() => setIsDiffModalOpen(true)}
|
||||
abortTask={abortTask}
|
||||
projectPath={projectPath}
|
||||
currentBranch={currentBranch}
|
||||
sessionProcessStatus={relatedSessionProcess?.status}
|
||||
revisionsData={revisionsData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -58,7 +58,7 @@ export const AssistantConversationContent: FC<{
|
||||
<div className="flex items-center gap-2">
|
||||
<Lightbulb className="h-4 w-4 text-muted-foreground group-hover:text-yellow-600 transition-colors" />
|
||||
<CardTitle className="text-sm font-medium group-hover:text-foreground transition-colors">
|
||||
<Trans id="assistant.thinking" message="Thinking" />
|
||||
<Trans id="assistant.thinking" />
|
||||
</CardTitle>
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180" />
|
||||
</div>
|
||||
@@ -115,10 +115,7 @@ export const AssistantConversationContent: FC<{
|
||||
data-testid="sidechain-task-button"
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
<Trans
|
||||
id="assistant.tool.view_task_details"
|
||||
message="View Task"
|
||||
/>
|
||||
<Trans id="assistant.tool.view_task_details" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@@ -157,7 +154,7 @@ export const AssistantConversationContent: FC<{
|
||||
<div className="space-y-3 py-3 px-4 border-t border-blue-200 dark:border-blue-800">
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-muted-foreground mb-1">
|
||||
<Trans id="assistant.tool.tool_id" message="Tool ID" />
|
||||
<Trans id="assistant.tool.tool_id" />
|
||||
</h4>
|
||||
<code className="text-xs bg-background/50 px-2 py-1 rounded border border-blue-200 dark:border-blue-800 font-mono">
|
||||
{content.id}
|
||||
@@ -165,10 +162,7 @@ export const AssistantConversationContent: FC<{
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-muted-foreground mb-2">
|
||||
<Trans
|
||||
id="assistant.tool.input_parameters"
|
||||
message="Input Parameters"
|
||||
/>
|
||||
<Trans id="assistant.tool.input_parameters" />
|
||||
</h4>
|
||||
<SyntaxHighlighter
|
||||
style={syntaxTheme}
|
||||
@@ -182,7 +176,7 @@ export const AssistantConversationContent: FC<{
|
||||
{toolResult && (
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-muted-foreground mb-2">
|
||||
<Trans id="assistant.tool.result" message="Tool Result" />
|
||||
<Trans id="assistant.tool.result" />
|
||||
</h4>
|
||||
<div className="bg-background rounded border p-3">
|
||||
{typeof toolResult.content === "string" ? (
|
||||
|
||||
@@ -35,7 +35,7 @@ const getConversationKey = (conversation: Conversation) => {
|
||||
}
|
||||
|
||||
if (conversation.type === "queue-operation") {
|
||||
return `queue-operation_${conversation.operation}_${conversation.sessionId}`;
|
||||
return `queue-operation_${conversation.operation}_${conversation.sessionId}_${conversation.timestamp}`;
|
||||
}
|
||||
|
||||
conversation satisfies never;
|
||||
@@ -52,10 +52,7 @@ const SchemaErrorDisplay: FC<{ errorLine: string }> = ({ errorLine }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-3 w-3 text-red-500" />
|
||||
<span className="text-xs font-medium text-red-600">
|
||||
<Trans
|
||||
id="conversation.error.schema"
|
||||
message="Schema Error"
|
||||
/>
|
||||
<Trans id="conversation.error.schema" />
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className="h-3 w-3 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||
@@ -70,36 +67,24 @@ const SchemaErrorDisplay: FC<{ errorLine: string }> = ({ errorLine }) => {
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle className="text-red-800">
|
||||
<Trans
|
||||
id="conversation.error.schema_validation"
|
||||
message="Schema Validation Error"
|
||||
/>
|
||||
<Trans id="conversation.error.schema_validation" />
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-red-700">
|
||||
<Trans
|
||||
id="conversation.error.schema_validation.description"
|
||||
message="This conversation entry failed to parse correctly. This might indicate a format change or parsing issue."
|
||||
/>{" "}
|
||||
<Trans id="conversation.error.schema_validation.description" />{" "}
|
||||
<a
|
||||
href="https://github.com/d-kimuson/claude-code-viewer/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-red-600 hover:text-red-800 underline underline-offset-4"
|
||||
>
|
||||
<Trans
|
||||
id="conversation.error.report_issue"
|
||||
message="Report this issue"
|
||||
/>
|
||||
<Trans id="conversation.error.report_issue" />
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="bg-gray-50 border rounded px-3 py-2">
|
||||
<h5 className="text-xs font-medium text-gray-700 mb-2">
|
||||
<Trans
|
||||
id="conversation.error.raw_content"
|
||||
message="Raw Content:"
|
||||
/>
|
||||
<Trans id="conversation.error.raw_content" />
|
||||
</h5>
|
||||
<pre className="text-xs overflow-x-auto whitespace-pre-wrap break-all font-mono text-gray-800">
|
||||
{errorLine}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const UserConversationContent: FC<{
|
||||
<div className="flex items-center gap-2">
|
||||
<ImageIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm font-medium">
|
||||
<Trans id="user.content.image" message="Image" />
|
||||
<Trans id="user.content.image" />
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
@@ -83,20 +83,14 @@ export const UserConversationContent: FC<{
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
||||
<CardTitle className="text-sm font-medium">
|
||||
<Trans
|
||||
id="user.content.unsupported_media"
|
||||
message="Unsupported Media"
|
||||
/>
|
||||
<Trans id="user.content.unsupported_media" />
|
||||
</CardTitle>
|
||||
<Badge variant="destructive">
|
||||
<Trans id="common.error" message="Error" />
|
||||
<Trans id="common.error" />
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="text-xs">
|
||||
<Trans
|
||||
id="user.content.unsupported_media.description"
|
||||
message="Media type not supported for display"
|
||||
/>
|
||||
<Trans id="user.content.unsupported_media.description" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -118,10 +112,7 @@ export const UserConversationContent: FC<{
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm font-medium">
|
||||
<Trans
|
||||
id="user.content.document.pdf"
|
||||
message="PDF Document"
|
||||
/>
|
||||
<Trans id="user.content.document.pdf" />
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
@@ -163,10 +154,7 @@ export const UserConversationContent: FC<{
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium">
|
||||
<Trans
|
||||
id="user.content.document.text"
|
||||
message="Text Document"
|
||||
/>
|
||||
<Trans id="user.content.document.text" />
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
@@ -201,20 +189,14 @@ export const UserConversationContent: FC<{
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
||||
<CardTitle className="text-sm font-medium">
|
||||
<Trans
|
||||
id="user.content.unsupported_document"
|
||||
message="Unsupported Document"
|
||||
/>
|
||||
<Trans id="user.content.unsupported_document" />
|
||||
</CardTitle>
|
||||
<Badge variant="destructive">
|
||||
<Trans id="common.error" message="Error" />
|
||||
<Trans id="common.error" />
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="text-xs">
|
||||
<Trans
|
||||
id="user.content.unsupported_document.description"
|
||||
message="Document type not supported for display"
|
||||
/>
|
||||
<Trans id="user.content.unsupported_document.description" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
@@ -64,8 +64,7 @@ export const SidechainConversationModal: FC<
|
||||
<div className="flex items-center gap-2 overflow-hidden w-full">
|
||||
<Eye className="h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span className="overflow-hidden text-ellipsis text-left flex-1">
|
||||
<Trans id="assistant.tool.view_task" message="View Task" />:{" "}
|
||||
{title}
|
||||
<Trans id="assistant.tool.view_task" />: {title}
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
@@ -94,7 +93,7 @@ export const SidechainConversationModal: FC<
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs flex items-center gap-2 flex-wrap">
|
||||
<span className="flex items-center gap-1">
|
||||
<Trans id="assistant.tool.task_id" message="Task ID" />:{" "}
|
||||
<Trans id="assistant.tool.task_id" />:{" "}
|
||||
<code className="px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono">
|
||||
{rootUuid.slice(0, 8)}
|
||||
</code>
|
||||
@@ -103,7 +102,6 @@ export const SidechainConversationModal: FC<
|
||||
<span>
|
||||
<Trans
|
||||
id="assistant.tool.message_count"
|
||||
message="{count} messages"
|
||||
values={{ count: messageCount }}
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -50,11 +50,10 @@ const DiffSummaryComponent: FC<DiffSummaryProps> = ({ summary, className }) => {
|
||||
<FileText className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
<span className="font-medium">
|
||||
<span className="hidden sm:inline">
|
||||
{summary.filesChanged}{" "}
|
||||
<Trans id="diff.files.changed" message="files changed" />
|
||||
{summary.filesChanged} <Trans id="diff.files.changed" />
|
||||
</span>
|
||||
<span className="sm:hidden">
|
||||
{summary.filesChanged} <Trans id="diff.files" message="files" />
|
||||
{summary.filesChanged} <Trans id="diff.files" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -417,7 +416,7 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
{isDiffLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
<Trans id="common.loading" message="Loading..." />
|
||||
<Trans id="common.loading" />
|
||||
</>
|
||||
) : (
|
||||
<RefreshCcwIcon className="w-4 h-4" />
|
||||
@@ -467,7 +466,7 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
className="w-full flex items-center justify-between p-2 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors rounded-t-lg"
|
||||
>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<Trans id="diff.commit.changes" message="Commit Changes" />
|
||||
<Trans id="diff.commit.changes" />
|
||||
</span>
|
||||
{isCommitSectionExpanded ? (
|
||||
<ChevronUp className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
@@ -488,7 +487,7 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
onClick={handleSelectAll}
|
||||
disabled={commitMutation.isPending}
|
||||
>
|
||||
<Trans id="diff.select.all" message="Select All" />
|
||||
<Trans id="diff.select.all" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -496,10 +495,7 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
onClick={handleDeselectAll}
|
||||
disabled={commitMutation.isPending}
|
||||
>
|
||||
<Trans
|
||||
id="diff.deselect.all"
|
||||
message="Deselect All"
|
||||
/>
|
||||
<Trans id="diff.deselect.all" />
|
||||
</Button>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{selectedCount} / {diffData.data.files.length} files
|
||||
@@ -539,10 +535,7 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
htmlFor={commitMessageId}
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<Trans
|
||||
id="diff.commit.message"
|
||||
message="Commit message"
|
||||
/>
|
||||
<Trans id="diff.commit.message" />
|
||||
</label>
|
||||
<Textarea
|
||||
id={commitMessageId}
|
||||
@@ -565,13 +558,10 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
{commitMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
<Trans
|
||||
id="diff.committing"
|
||||
message="Committing..."
|
||||
/>
|
||||
<Trans id="diff.committing" />
|
||||
</>
|
||||
) : (
|
||||
<Trans id="diff.commit" message="Commit" />
|
||||
<Trans id="diff.commit" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -583,10 +573,10 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
{pushMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
<Trans id="diff.pushing" message="Pushing..." />
|
||||
<Trans id="diff.pushing" />
|
||||
</>
|
||||
) : (
|
||||
<Trans id="diff.push" message="Push" />
|
||||
<Trans id="diff.push" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -600,16 +590,10 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
{commitAndPushMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
<Trans
|
||||
id="diff.committing.pushing"
|
||||
message="Committing & Pushing..."
|
||||
/>
|
||||
<Trans id="diff.committing.pushing" />
|
||||
</>
|
||||
) : (
|
||||
<Trans
|
||||
id="diff.commit.push"
|
||||
message="Commit & Push"
|
||||
/>
|
||||
<Trans id="diff.commit.push" />
|
||||
)}
|
||||
</Button>
|
||||
{isCommitDisabled &&
|
||||
@@ -617,15 +601,9 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
!commitAndPushMutation.isPending && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{selectedCount === 0 ? (
|
||||
<Trans
|
||||
id="diff.select.file"
|
||||
message="Select at least one file"
|
||||
/>
|
||||
<Trans id="diff.select.file" />
|
||||
) : (
|
||||
<Trans
|
||||
id="diff.enter.message"
|
||||
message="Enter a commit message"
|
||||
/>
|
||||
<Trans id="diff.enter.message" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
@@ -661,7 +639,7 @@ export const DiffModal: FC<DiffModalProps> = ({
|
||||
<div className="text-center space-y-2">
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto" />
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<Trans id="diff.loading" message="Loading diff..." />
|
||||
<Trans id="diff.loading" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Trans, useLingui } from "@lingui/react";
|
||||
import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
GitCompareIcon,
|
||||
LoaderIcon,
|
||||
PlusIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { PublicSessionProcess } from "../../../../../../../types/session-process";
|
||||
import { NewChatModal } from "../../../../components/newChat/NewChatModal";
|
||||
|
||||
interface ChatActionMenuProps {
|
||||
projectId: string;
|
||||
isPending?: boolean;
|
||||
onScrollToTop?: () => void;
|
||||
onScrollToBottom?: () => void;
|
||||
onOpenDiffModal?: () => void;
|
||||
sessionProcess?: PublicSessionProcess;
|
||||
abortTask?: UseMutationResult<unknown, Error, string, unknown>;
|
||||
}
|
||||
|
||||
export const ChatActionMenu: FC<ChatActionMenuProps> = ({
|
||||
projectId,
|
||||
isPending = false,
|
||||
onScrollToTop,
|
||||
onScrollToBottom,
|
||||
onOpenDiffModal,
|
||||
sessionProcess,
|
||||
abortTask,
|
||||
}) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
return (
|
||||
<div className="px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 mb-1">
|
||||
<div className="py-0 flex items-center gap-1.5 flex-wrap">
|
||||
{onOpenDiffModal && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onOpenDiffModal}
|
||||
disabled={isPending}
|
||||
className="h-7 px-2 gap-1.5 text-xs bg-muted/20 rounded-lg border border-border/40"
|
||||
title={i18n._({
|
||||
id: "control.open_git_dialog",
|
||||
message: "Open Git Dialog",
|
||||
})}
|
||||
>
|
||||
<GitCompareIcon className="w-3.5 h-3.5" />
|
||||
<span>
|
||||
<Trans id="control.git" />
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
<NewChatModal
|
||||
projectId={projectId}
|
||||
trigger={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={isPending}
|
||||
className="h-7 px-2 gap-1.5 text-xs bg-muted/20 rounded-lg border border-border/40"
|
||||
title={i18n._({
|
||||
id: "control.new_chat",
|
||||
message: "New Chat",
|
||||
})}
|
||||
>
|
||||
<PlusIcon className="w-3.5 h-3.5" />
|
||||
<span>
|
||||
<Trans id="control.new" />
|
||||
</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{onScrollToTop && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onScrollToTop}
|
||||
disabled={isPending}
|
||||
className="h-7 px-2 gap-1.5 text-xs bg-muted/20 rounded-lg border border-border/40"
|
||||
title={i18n._({
|
||||
id: "control.scroll_to_top",
|
||||
message: "Scroll to Top",
|
||||
})}
|
||||
>
|
||||
<ArrowUpIcon className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
{onScrollToBottom && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onScrollToBottom}
|
||||
disabled={isPending}
|
||||
className="h-7 px-2 gap-1.5 text-xs bg-muted/20 rounded-lg border border-border/40"
|
||||
title={i18n._({
|
||||
id: "control.scroll_to_bottom",
|
||||
message: "Scroll to Bottom",
|
||||
})}
|
||||
>
|
||||
<ArrowDownIcon className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
{sessionProcess && abortTask && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
abortTask.mutate(sessionProcess.id);
|
||||
}}
|
||||
disabled={abortTask.isPending || isPending}
|
||||
className="h-7 px-2 gap-1.5 text-xs bg-muted/20 rounded-lg border border-border/40"
|
||||
>
|
||||
{abortTask.isPending ? (
|
||||
<LoaderIcon className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<XIcon className="w-3.5 h-3.5" />
|
||||
)}
|
||||
<span>
|
||||
<Trans id="session.conversation.abort" />
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,8 @@ export const ContinueChat: FC<{
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
sessionProcessId: string;
|
||||
}> = ({ projectId, sessionId, sessionProcessId }) => {
|
||||
sessionProcessStatus?: "running" | "paused";
|
||||
}> = ({ projectId, sessionId, sessionProcessId, sessionProcessStatus }) => {
|
||||
const { i18n } = useLingui();
|
||||
const continueSessionProcess = useContinueSessionProcessMutation(
|
||||
projectId,
|
||||
@@ -29,43 +30,40 @@ export const ContinueChat: FC<{
|
||||
return i18n._({
|
||||
id: "chat.placeholder.continue.enter",
|
||||
message:
|
||||
"Type your message... (Start with / for commands, @ for files, Enter to send, or schedule for later)",
|
||||
"Type your message... (Start with / for commands, @ for files, Enter to send)",
|
||||
});
|
||||
}
|
||||
if (behavior === "command-enter-send") {
|
||||
return i18n._({
|
||||
id: "chat.placeholder.continue.command_enter",
|
||||
message:
|
||||
"Type your message... (Start with / for commands, @ for files, Command+Enter to send, or schedule for later)",
|
||||
"Type your message... (Start with / for commands, @ for files, Command+Enter to send)",
|
||||
});
|
||||
}
|
||||
return i18n._({
|
||||
id: "chat.placeholder.continue.shift_enter",
|
||||
message:
|
||||
"Type your message... (Start with / for commands, @ for files, Shift+Enter to send, or schedule for later)",
|
||||
"Type your message... (Start with / for commands, @ for files, Shift+Enter to send)",
|
||||
});
|
||||
};
|
||||
|
||||
const buttonText = <Trans id="chat.send" message="Send" />;
|
||||
const isRunning = sessionProcessStatus === "running";
|
||||
|
||||
return (
|
||||
<div className="relative mt-8 mb-6">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-border to-transparent h-px top-0" />
|
||||
<div className="pt-8">
|
||||
<ChatInput
|
||||
projectId={projectId}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={continueSessionProcess.isPending}
|
||||
error={continueSessionProcess.error}
|
||||
placeholder={getPlaceholder()}
|
||||
buttonText={buttonText}
|
||||
minHeight="min-h-[120px]"
|
||||
containerClassName=""
|
||||
buttonSize="lg"
|
||||
enableScheduledSend={true}
|
||||
baseSessionId={sessionId}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 pb-3">
|
||||
<ChatInput
|
||||
projectId={projectId}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={continueSessionProcess.isPending}
|
||||
error={continueSessionProcess.error}
|
||||
placeholder={getPlaceholder()}
|
||||
buttonText={<Trans id="chat.send" />}
|
||||
containerClassName=""
|
||||
buttonSize="default"
|
||||
enableScheduledSend={!isRunning}
|
||||
baseSessionId={sessionId}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,43 +28,39 @@ export const ResumeChat: FC<{
|
||||
return i18n._({
|
||||
id: "chat.placeholder.resume.enter",
|
||||
message:
|
||||
"Type your message... (Start with / for commands, @ for files, Enter to send, or schedule for later)",
|
||||
"Type your message... (Start with / for commands, @ for files, Enter to send)",
|
||||
});
|
||||
}
|
||||
if (behavior === "command-enter-send") {
|
||||
return i18n._({
|
||||
id: "chat.placeholder.resume.command_enter",
|
||||
message:
|
||||
"Type your message... (Start with / for commands, @ for files, Command+Enter to send, or schedule for later)",
|
||||
"Type your message... (Start with / for commands, @ for files, Command+Enter to send)",
|
||||
});
|
||||
}
|
||||
return i18n._({
|
||||
id: "chat.placeholder.resume.shift_enter",
|
||||
message:
|
||||
"Type your message... (Start with / for commands, @ for files, Shift+Enter to send, or schedule for later)",
|
||||
"Type your message... (Start with / for commands, @ for files, Shift+Enter to send)",
|
||||
});
|
||||
};
|
||||
|
||||
const buttonText = <Trans id="chat.resume" message="Resume" />;
|
||||
const buttonText = <Trans id="chat.resume" />;
|
||||
|
||||
return (
|
||||
<div className="relative mt-8 mb-6">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-border to-transparent h-px top-0" />
|
||||
<div className="pt-8">
|
||||
<ChatInput
|
||||
projectId={projectId}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={createSessionProcess.isPending}
|
||||
error={createSessionProcess.error}
|
||||
placeholder={getPlaceholder()}
|
||||
buttonText={buttonText}
|
||||
minHeight="min-h-[120px]"
|
||||
containerClassName=""
|
||||
buttonSize="lg"
|
||||
enableScheduledSend={true}
|
||||
baseSessionId={sessionId}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 pb-3">
|
||||
<ChatInput
|
||||
projectId={projectId}
|
||||
onSubmit={handleSubmit}
|
||||
isPending={createSessionProcess.isPending}
|
||||
error={createSessionProcess.error}
|
||||
placeholder={getPlaceholder()}
|
||||
buttonText={buttonText}
|
||||
containerClassName=""
|
||||
buttonSize="default"
|
||||
enableScheduledSend={true}
|
||||
baseSessionId={sessionId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,31 +27,31 @@ interface ParsedCron {
|
||||
const WEEKDAYS = [
|
||||
{
|
||||
value: 0,
|
||||
labelKey: <Trans id="cron_builder.sunday" message="Sunday" />,
|
||||
labelKey: <Trans id="cron_builder.sunday" />,
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
labelKey: <Trans id="cron_builder.monday" message="Monday" />,
|
||||
labelKey: <Trans id="cron_builder.monday" />,
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
labelKey: <Trans id="cron_builder.tuesday" message="Tuesday" />,
|
||||
labelKey: <Trans id="cron_builder.tuesday" />,
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
labelKey: <Trans id="cron_builder.wednesday" message="Wednesday" />,
|
||||
labelKey: <Trans id="cron_builder.wednesday" />,
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
labelKey: <Trans id="cron_builder.thursday" message="Thursday" />,
|
||||
labelKey: <Trans id="cron_builder.thursday" />,
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
labelKey: <Trans id="cron_builder.friday" message="Friday" />,
|
||||
labelKey: <Trans id="cron_builder.friday" />,
|
||||
},
|
||||
{
|
||||
value: 6,
|
||||
labelKey: <Trans id="cron_builder.saturday" message="Saturday" />,
|
||||
labelKey: <Trans id="cron_builder.saturday" />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -207,7 +207,7 @@ export function CronExpressionBuilder({
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Trans id="cron_builder.schedule_type" message="Schedule Type" />
|
||||
<Trans id="cron_builder.schedule_type" />
|
||||
</Label>
|
||||
<Select value={mode} onValueChange={handleModeChange}>
|
||||
<SelectTrigger>
|
||||
@@ -215,16 +215,16 @@ export function CronExpressionBuilder({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hourly">
|
||||
<Trans id="cron_builder.hourly" message="Hourly" />
|
||||
<Trans id="cron_builder.hourly" />
|
||||
</SelectItem>
|
||||
<SelectItem value="daily">
|
||||
<Trans id="cron_builder.daily" message="Daily" />
|
||||
<Trans id="cron_builder.daily" />
|
||||
</SelectItem>
|
||||
<SelectItem value="weekly">
|
||||
<Trans id="cron_builder.weekly" message="Weekly" />
|
||||
<Trans id="cron_builder.weekly" />
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">
|
||||
<Trans id="cron_builder.custom" message="Custom" />
|
||||
<Trans id="cron_builder.custom" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -234,7 +234,7 @@ export function CronExpressionBuilder({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Trans id="cron_builder.hour" message="Hour (0-23)" />
|
||||
<Trans id="cron_builder.hour" />
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -246,7 +246,7 @@ export function CronExpressionBuilder({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Trans id="cron_builder.minute" message="Minute (0-59)" />
|
||||
<Trans id="cron_builder.minute" />
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -263,7 +263,7 @@ export function CronExpressionBuilder({
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Trans id="cron_builder.day_of_week" message="Day of Week" />
|
||||
<Trans id="cron_builder.day_of_week" />
|
||||
</Label>
|
||||
<Select
|
||||
value={String(dayOfWeek)}
|
||||
@@ -284,7 +284,7 @@ export function CronExpressionBuilder({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Trans id="cron_builder.hour" message="Hour (0-23)" />
|
||||
<Trans id="cron_builder.hour" />
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -296,7 +296,7 @@ export function CronExpressionBuilder({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Trans id="cron_builder.minute" message="Minute (0-59)" />
|
||||
<Trans id="cron_builder.minute" />
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -313,10 +313,7 @@ export function CronExpressionBuilder({
|
||||
{mode === "custom" && (
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Trans
|
||||
id="cron_builder.cron_expression"
|
||||
message="Cron Expression"
|
||||
/>
|
||||
<Trans id="cron_builder.cron_expression" />
|
||||
</Label>
|
||||
<Input
|
||||
value={customExpression}
|
||||
@@ -328,7 +325,7 @@ export function CronExpressionBuilder({
|
||||
|
||||
<div className="rounded-md border p-3 text-sm">
|
||||
<div className="font-medium mb-1">
|
||||
<Trans id="cron_builder.preview" message="Preview" />
|
||||
<Trans id="cron_builder.preview" />
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{error ? (
|
||||
@@ -340,7 +337,7 @@ export function CronExpressionBuilder({
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
<Trans id="cron_builder.expression" message="Expression" />:{" "}
|
||||
<Trans id="cron_builder.expression" />:{" "}
|
||||
<code>{mode === "custom" ? customExpression : value}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -161,22 +161,13 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{job ? (
|
||||
<Trans
|
||||
id="scheduler.dialog.title.edit"
|
||||
message="Edit Scheduled Job"
|
||||
/>
|
||||
<Trans id="scheduler.dialog.title.edit" />
|
||||
) : (
|
||||
<Trans
|
||||
id="scheduler.dialog.title.create"
|
||||
message="Create Scheduled Job"
|
||||
/>
|
||||
<Trans id="scheduler.dialog.title.create" />
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans
|
||||
id="scheduler.dialog.description"
|
||||
message="Set up a scheduled job to send messages to Claude Code"
|
||||
/>
|
||||
<Trans id="scheduler.dialog.description" />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -185,13 +176,10 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="enabled" className="text-base font-semibold">
|
||||
<Trans id="scheduler.form.enabled" message="Enabled" />
|
||||
<Trans id="scheduler.form.enabled" />
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans
|
||||
id="scheduler.form.enabled.description"
|
||||
message="Enable or disable this scheduled job"
|
||||
/>
|
||||
<Trans id="scheduler.form.enabled.description" />
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -205,7 +193,7 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
{/* Job Name */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="job-name">
|
||||
<Trans id="scheduler.form.name" message="Job Name" />
|
||||
<Trans id="scheduler.form.name" />
|
||||
</Label>
|
||||
<Input
|
||||
id="job-name"
|
||||
@@ -222,10 +210,7 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
{/* Schedule Type */}
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Trans
|
||||
id="scheduler.form.schedule_type"
|
||||
message="Schedule Type"
|
||||
/>
|
||||
<Trans id="scheduler.form.schedule_type" />
|
||||
</Label>
|
||||
<Select
|
||||
value={scheduleType}
|
||||
@@ -239,16 +224,10 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="cron">
|
||||
<Trans
|
||||
id="scheduler.form.schedule_type.cron"
|
||||
message="Recurring (Cron)"
|
||||
/>
|
||||
<Trans id="scheduler.form.schedule_type.cron" />
|
||||
</SelectItem>
|
||||
<SelectItem value="reserved">
|
||||
<Trans
|
||||
id="scheduler.form.schedule_type.reserved"
|
||||
message="One-time"
|
||||
/>
|
||||
<Trans id="scheduler.form.schedule_type.reserved" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -263,10 +242,7 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reserved-datetime">
|
||||
<Trans
|
||||
id="scheduler.form.reserved_time"
|
||||
message="Scheduled Date and Time"
|
||||
/>
|
||||
<Trans id="scheduler.form.reserved_time" />
|
||||
</Label>
|
||||
<Input
|
||||
id="reserved-datetime"
|
||||
@@ -276,10 +252,7 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans
|
||||
id="scheduler.form.reserved_time.hint"
|
||||
message="Will run once at the specified time, then be automatically deleted"
|
||||
/>
|
||||
<Trans id="scheduler.form.reserved_time.hint" />
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -287,7 +260,7 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
{/* Message Content */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="message-content">
|
||||
<Trans id="scheduler.form.message" message="Message Content" />
|
||||
<Trans id="scheduler.form.message" />
|
||||
</Label>
|
||||
<div className="relative" ref={completion.containerRef}>
|
||||
<Textarea
|
||||
@@ -333,10 +306,7 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans
|
||||
id="scheduler.form.message.hint"
|
||||
message="/ for commands, @ for files"
|
||||
/>
|
||||
<Trans id="scheduler.form.message.hint" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -344,10 +314,7 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
{scheduleType === "cron" && (
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Trans
|
||||
id="scheduler.form.concurrency_policy"
|
||||
message="Concurrency Policy"
|
||||
/>
|
||||
<Trans id="scheduler.form.concurrency_policy" />
|
||||
</Label>
|
||||
<Select
|
||||
value={concurrencyPolicy}
|
||||
@@ -361,16 +328,10 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="skip">
|
||||
<Trans
|
||||
id="scheduler.form.concurrency_policy.skip"
|
||||
message="Skip if running"
|
||||
/>
|
||||
<Trans id="scheduler.form.concurrency_policy.skip" />
|
||||
</SelectItem>
|
||||
<SelectItem value="run">
|
||||
<Trans
|
||||
id="scheduler.form.concurrency_policy.run"
|
||||
message="Run even if running"
|
||||
/>
|
||||
<Trans id="scheduler.form.concurrency_policy.run" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -384,18 +345,18 @@ export const SchedulerJobDialog: FC<SchedulerJobDialogProps> = ({
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Trans id="common.cancel" message="Cancel" />
|
||||
<Trans id="common.cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!isFormValid || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Trans id="common.saving" message="Saving..." />
|
||||
<Trans id="common.saving" />
|
||||
) : job ? (
|
||||
<Trans id="common.update" message="Update" />
|
||||
<Trans id="common.update" />
|
||||
) : (
|
||||
<Trans id="common.create" message="Create" />
|
||||
<Trans id="common.create" />
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -35,7 +35,7 @@ export const McpTab: FC<{ projectId: string }> = ({ projectId }) => {
|
||||
<div className="p-3 border-b border-sidebar-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-sidebar-foreground">
|
||||
<Trans id="mcp.title" message="MCP Servers" />
|
||||
<Trans id="mcp.title" />
|
||||
</h2>
|
||||
<Button
|
||||
onClick={handleReload}
|
||||
@@ -65,7 +65,6 @@ export const McpTab: FC<{ projectId: string }> = ({ projectId }) => {
|
||||
<div className="text-sm text-red-500">
|
||||
<Trans
|
||||
id="mcp.error.load_failed"
|
||||
message="Failed to load MCP servers: {error}"
|
||||
values={{ error: (error as Error).message }}
|
||||
/>
|
||||
</div>
|
||||
@@ -73,7 +72,7 @@ export const McpTab: FC<{ projectId: string }> = ({ projectId }) => {
|
||||
|
||||
{mcpData && mcpData.servers.length === 0 && (
|
||||
<div className="text-sm text-muted-foreground text-center py-8">
|
||||
<Trans id="mcp.no.servers" message="No MCP servers found" />
|
||||
<Trans id="mcp.no.servers" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -99,10 +99,7 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
fallback={
|
||||
<div className="flex-1 flex items-center justify-center p-4">
|
||||
<div className="text-sm text-sidebar-foreground/70">
|
||||
<Trans
|
||||
id="settings.loading"
|
||||
message="Loading settings..."
|
||||
/>
|
||||
<Trans id="settings.loading" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -110,20 +107,14 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium text-sm text-sidebar-foreground">
|
||||
<Trans
|
||||
id="settings.session.display"
|
||||
message="Session Display"
|
||||
/>
|
||||
<Trans id="settings.session.display" />
|
||||
</h3>
|
||||
<SettingsControls openingProjectId={projectId} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium text-sm text-sidebar-foreground">
|
||||
<Trans
|
||||
id="settings.notifications"
|
||||
message="Notifications"
|
||||
/>
|
||||
<Trans id="settings.notifications" />
|
||||
</h3>
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
@@ -183,10 +174,7 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
<Trans
|
||||
id="sidebar.back.to.projects"
|
||||
message="Back to projects"
|
||||
/>
|
||||
<Trans id="sidebar.back.to.projects" />
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -211,10 +199,7 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
<Trans
|
||||
id="sidebar.show.session.list"
|
||||
message="Show session list"
|
||||
/>
|
||||
<Trans id="sidebar.show.session.list" />
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -238,10 +223,7 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
<Trans
|
||||
id="sidebar.show.mcp.settings"
|
||||
message="Show MCP server settings"
|
||||
/>
|
||||
<Trans id="sidebar.show.mcp.settings" />
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -264,10 +246,7 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<Trans
|
||||
id="settings.tab.title"
|
||||
message="Settings for display and notifications"
|
||||
/>
|
||||
<Trans id="settings.tab.title" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -289,10 +268,7 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<Trans
|
||||
id="system.info.tab.title"
|
||||
message="Show system information"
|
||||
/>
|
||||
<Trans id="system.info.tab.title" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +163,7 @@ export const SchedulerTab: FC<{ projectId: string; sessionId: string }> = ({
|
||||
<div className="p-3 border-b border-sidebar-border">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-sidebar-foreground">
|
||||
<Trans id="scheduler.title" message="Scheduler" />
|
||||
<Trans id="scheduler.title" />
|
||||
</h2>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
@@ -210,7 +210,6 @@ export const SchedulerTab: FC<{ projectId: string; sessionId: string }> = ({
|
||||
<div className="text-sm text-red-500">
|
||||
<Trans
|
||||
id="scheduler.error.load_failed"
|
||||
message="Failed to load scheduler jobs: {error}"
|
||||
values={{ error: error.message }}
|
||||
/>
|
||||
</div>
|
||||
@@ -218,10 +217,7 @@ export const SchedulerTab: FC<{ projectId: string; sessionId: string }> = ({
|
||||
|
||||
{jobs && jobs.length === 0 && (
|
||||
<div className="text-sm text-muted-foreground text-center py-8">
|
||||
<Trans
|
||||
id="scheduler.no_jobs"
|
||||
message="No scheduled jobs. Click + to create one."
|
||||
/>
|
||||
<Trans id="scheduler.no_jobs" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -243,15 +239,9 @@ export const SchedulerTab: FC<{ projectId: string; sessionId: string }> = ({
|
||||
className="text-xs"
|
||||
>
|
||||
{job.enabled ? (
|
||||
<Trans
|
||||
id="scheduler.status.enabled"
|
||||
message="Enabled"
|
||||
/>
|
||||
<Trans id="scheduler.status.enabled" />
|
||||
) : (
|
||||
<Trans
|
||||
id="scheduler.status.disabled"
|
||||
message="Disabled"
|
||||
/>
|
||||
<Trans id="scheduler.status.disabled" />
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -283,7 +273,7 @@ export const SchedulerTab: FC<{ projectId: string; sessionId: string }> = ({
|
||||
<div className="text-xs text-muted-foreground mt-2 pt-2 border-t border-sidebar-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>
|
||||
<Trans id="scheduler.last_run" message="Last run: " />
|
||||
<Trans id="scheduler.last_run" />
|
||||
<span>{formatLastRun(job.lastRunAt)}</span>
|
||||
</span>
|
||||
{job.lastRunStatus && (
|
||||
@@ -326,13 +316,10 @@ export const SchedulerTab: FC<{ projectId: string; sessionId: string }> = ({
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans id="scheduler.delete_dialog.title" message="Delete Job" />
|
||||
<Trans id="scheduler.delete_dialog.title" />
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans
|
||||
id="scheduler.delete_dialog.description"
|
||||
message="Are you sure you want to delete this job? This action cannot be undone."
|
||||
/>
|
||||
<Trans id="scheduler.delete_dialog.description" />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
@@ -344,7 +331,7 @@ export const SchedulerTab: FC<{ projectId: string; sessionId: string }> = ({
|
||||
}}
|
||||
disabled={deleteJob.isPending}
|
||||
>
|
||||
<Trans id="common.cancel" message="Cancel" />
|
||||
<Trans id="common.cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -352,9 +339,9 @@ export const SchedulerTab: FC<{ projectId: string; sessionId: string }> = ({
|
||||
disabled={deleteJob.isPending}
|
||||
>
|
||||
{deleteJob.isPending ? (
|
||||
<Trans id="common.deleting" message="Deleting..." />
|
||||
<Trans id="common.deleting" />
|
||||
) : (
|
||||
<Trans id="common.delete" message="Delete" />
|
||||
<Trans id="common.delete" />
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { McpTab } from "./McpTab";
|
||||
import { MobileSidebar } from "./MobileSidebar";
|
||||
import { SchedulerTab } from "./SchedulerTab";
|
||||
import { SessionsTab } from "./SessionsTab";
|
||||
import type { Tab } from "./schema";
|
||||
|
||||
export const SessionSidebar: FC<{
|
||||
currentSessionId: string;
|
||||
@@ -28,21 +29,21 @@ export const SessionSidebar: FC<{
|
||||
className?: string;
|
||||
isMobileOpen?: boolean;
|
||||
onMobileOpenChange?: (open: boolean) => void;
|
||||
initialTab: Tab;
|
||||
}> = ({
|
||||
currentSessionId,
|
||||
projectId,
|
||||
className,
|
||||
isMobileOpen = false,
|
||||
onMobileOpenChange,
|
||||
initialTab,
|
||||
}) => {
|
||||
const additionalTabs: SidebarTab[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "sessions",
|
||||
icon: MessageSquareIcon,
|
||||
title: (
|
||||
<Trans id="sidebar.show.session.list" message="Show session list" />
|
||||
),
|
||||
title: <Trans id="sidebar.show.session.list" />,
|
||||
content: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SessionsTab
|
||||
@@ -55,23 +56,13 @@ export const SessionSidebar: FC<{
|
||||
{
|
||||
id: "mcp",
|
||||
icon: PlugIcon,
|
||||
title: (
|
||||
<Trans
|
||||
id="sidebar.show.mcp.settings"
|
||||
message="Show MCP server settings"
|
||||
/>
|
||||
),
|
||||
title: <Trans id="sidebar.show.mcp.settings" />,
|
||||
content: <McpTab projectId={projectId} />,
|
||||
},
|
||||
{
|
||||
id: "scheduler",
|
||||
icon: CalendarClockIcon,
|
||||
title: (
|
||||
<Trans
|
||||
id="sidebar.show.scheduler.jobs"
|
||||
message="Show scheduler jobs"
|
||||
/>
|
||||
),
|
||||
title: <Trans id="sidebar.show.scheduler.jobs" />,
|
||||
content: (
|
||||
<SchedulerTab projectId={projectId} sessionId={currentSessionId} />
|
||||
),
|
||||
@@ -87,7 +78,7 @@ export const SessionSidebar: FC<{
|
||||
<GlobalSidebar
|
||||
projectId={projectId}
|
||||
additionalTabs={additionalTabs}
|
||||
defaultActiveTab="sessions"
|
||||
defaultActiveTab={initialTab}
|
||||
headerButton={
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -101,10 +92,7 @@ export const SessionSidebar: FC<{
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
<Trans
|
||||
id="sidebar.back.to.projects"
|
||||
message="Back to projects"
|
||||
/>
|
||||
<Trans id="sidebar.back.to.projects" />
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from "@lingui/react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Link, useSearch } from "@tanstack/react-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { MessageSquareIcon, PlusIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
@@ -28,6 +28,12 @@ export const SessionsTab: FC<{
|
||||
|
||||
const sessionProcesses = useAtomValue(sessionProcessesAtom);
|
||||
const { config } = useConfig();
|
||||
const search = useSearch({
|
||||
from: "/projects/$projectId/sessions/$sessionId/",
|
||||
});
|
||||
|
||||
// Preserve current tab state or default to "sessions"
|
||||
const currentTab = search.tab ?? "sessions";
|
||||
|
||||
// Sort sessions: Running > Paused > Others, then by lastModifiedAt (newest first)
|
||||
const sortedSessions = [...sessions].sort((a, b) => {
|
||||
@@ -67,7 +73,7 @@ export const SessionsTab: FC<{
|
||||
<div className="border-b border-sidebar-border p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg">
|
||||
<Trans id="sessions.title" message="Sessions" />
|
||||
<Trans id="sessions.title" />
|
||||
</h2>
|
||||
<NewChatModal
|
||||
projectId={projectId}
|
||||
@@ -83,13 +89,13 @@ export const SessionsTab: FC<{
|
||||
}
|
||||
>
|
||||
<PlusIcon className="w-3.5 h-3.5" />
|
||||
<Trans id="sessions.new" message="New" />
|
||||
<Trans id="sessions.new" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-sidebar-foreground/70">
|
||||
{sessions.length} <Trans id="sessions.total" message="total" />
|
||||
{sessions.length} <Trans id="sessions.total" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -112,6 +118,7 @@ export const SessionsTab: FC<{
|
||||
key={session.id}
|
||||
to={"/projects/$projectId/sessions/$sessionId"}
|
||||
params={{ projectId, sessionId: session.id }}
|
||||
search={{ tab: currentTab }}
|
||||
className={cn(
|
||||
"block rounded-lg p-2.5 transition-all duration-200 hover:bg-blue-50/60 dark:hover:bg-blue-950/40 hover:border-blue-300/60 dark:hover:border-blue-700/60 hover:shadow-sm border border-sidebar-border/40 bg-sidebar/30",
|
||||
isActive &&
|
||||
@@ -133,9 +140,9 @@ export const SessionsTab: FC<{
|
||||
)}
|
||||
>
|
||||
{isRunning ? (
|
||||
<Trans id="session.status.running" message="Running" />
|
||||
<Trans id="session.status.running" />
|
||||
) : (
|
||||
<Trans id="session.status.paused" message="Paused" />
|
||||
<Trans id="session.status.paused" />
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -170,9 +177,9 @@ export const SessionsTab: FC<{
|
||||
className="w-full"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<Trans id="common.loading" message="Loading..." />
|
||||
<Trans id="common.loading" />
|
||||
) : (
|
||||
<Trans id="sessions.load.more" message="Load More" />
|
||||
<Trans id="sessions.load.more" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const tabSchema = z.enum([
|
||||
"sessions",
|
||||
"mcp",
|
||||
"scheduler",
|
||||
"settings",
|
||||
"system-info",
|
||||
]);
|
||||
|
||||
export type Tab = z.infer<typeof tabSchema>;
|
||||
@@ -38,7 +38,7 @@ export const DirectoryPicker: FC<DirectoryPickerProps> = ({ onPathChange }) => {
|
||||
<div className="border rounded-md">
|
||||
<div className="p-3 border-b bg-muted/50">
|
||||
<p className="text-sm font-medium">
|
||||
<Trans id="directory_picker.current" message="Current:" />{" "}
|
||||
<Trans id="directory_picker.current" />{" "}
|
||||
<span className="font-mono">{data?.currentPath || "~"}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -49,16 +49,13 @@ export const DirectoryPicker: FC<DirectoryPickerProps> = ({ onPathChange }) => {
|
||||
onCheckedChange={(checked) => setShowHidden(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="show-hidden" className="text-sm cursor-pointer">
|
||||
<Trans
|
||||
id="directory_picker.show_hidden"
|
||||
message="Show hidden files"
|
||||
/>
|
||||
<Trans id="directory_picker.show_hidden" />
|
||||
</Label>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{isLoading ? (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
<Trans id="directory_picker.loading" message="Loading..." />
|
||||
<Trans id="directory_picker.loading" />
|
||||
</div>
|
||||
) : data?.entries && data.entries.length > 0 ? (
|
||||
<div className="divide-y">
|
||||
@@ -88,10 +85,7 @@ export const DirectoryPicker: FC<DirectoryPickerProps> = ({ onPathChange }) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
<Trans
|
||||
id="directory_picker.no_directories"
|
||||
message="No directories found"
|
||||
/>
|
||||
<Trans id="directory_picker.no_directories" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -25,16 +25,10 @@ export const ProjectList: FC = () => {
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<FolderIcon className="w-12 h-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">
|
||||
<Trans
|
||||
id="project_list.no_projects.title"
|
||||
message="No projects found"
|
||||
/>
|
||||
<Trans id="project_list.no_projects.title" />
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md">
|
||||
<Trans
|
||||
id="project_list.no_projects.description"
|
||||
message="No Claude Code projects found in your ~/.claude/projects directory. Start a conversation with Claude Code to create your first project."
|
||||
/>
|
||||
<Trans id="project_list.no_projects.description" />
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>;
|
||||
@@ -57,7 +51,7 @@ export const ProjectList: FC = () => {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans id="project_list.last_modified" message="Last modified:" />{" "}
|
||||
<Trans id="project_list.last_modified" />{" "}
|
||||
{project.lastModifiedAt
|
||||
? formatLocaleDate(project.lastModifiedAt, {
|
||||
locale: config.locale,
|
||||
@@ -66,8 +60,7 @@ export const ProjectList: FC = () => {
|
||||
: ""}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans id="project_list.messages" message="Messages:" />{" "}
|
||||
{project.meta.sessionCount}
|
||||
<Trans id="project_list.messages" /> {project.meta.sessionCount}
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardContent className="pt-0">
|
||||
@@ -76,10 +69,7 @@ export const ProjectList: FC = () => {
|
||||
to={"/projects/$projectId/latest"}
|
||||
params={{ projectId: project.id }}
|
||||
>
|
||||
<Trans
|
||||
id="project_list.view_conversations"
|
||||
message="View Conversations"
|
||||
/>
|
||||
<Trans id="project_list.view_conversations" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -59,18 +59,17 @@ export const SetupProjectDialog: FC = () => {
|
||||
<DialogTrigger asChild>
|
||||
<Button data-testid="new-project-button">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
<Trans id="project.new" message="New Project" />
|
||||
<Trans id="project.new" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl" data-testid="new-project-modal">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans id="project.setup.title" message="Setup New Project" />
|
||||
<Trans id="project.setup.title" />
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans
|
||||
id="project.setup.description"
|
||||
message="Navigate to a directory to set up as a Claude Code project. If CLAUDE.md exists, the project will be described. Otherwise, <0>/init</0> will be run to initialize it."
|
||||
components={{
|
||||
0: <code className="text-sm bg-muted px-1 py-0.5 rounded" />,
|
||||
}}
|
||||
@@ -82,7 +81,7 @@ export const SetupProjectDialog: FC = () => {
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
<Trans id="common.action.cancel" message="Cancel" />
|
||||
<Trans id="common.action.cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => await setupProjectMutation.mutateAsync()}
|
||||
@@ -91,13 +90,10 @@ export const SetupProjectDialog: FC = () => {
|
||||
{setupProjectMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
<Trans
|
||||
id="project.setup.action.setting_up"
|
||||
message="Setting up..."
|
||||
/>
|
||||
<Trans id="project.setup.action.setting_up" />
|
||||
</>
|
||||
) : (
|
||||
<Trans id="project.setup.action.setup" message="Setup Project" />
|
||||
<Trans id="project.setup.action.setup" />
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -17,10 +17,7 @@ export const ProjectsPage: FC = () => {
|
||||
Claude Code Viewer
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
<Trans
|
||||
id="projects.page.description"
|
||||
message="Browse your Claude Code conversation history and project interactions"
|
||||
/>
|
||||
<Trans id="projects.page.description" />
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -28,7 +25,7 @@ export const ProjectsPage: FC = () => {
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold">
|
||||
<Trans id="projects.page.title" message="Your Projects" />
|
||||
<Trans id="projects.page.title" />
|
||||
</h2>
|
||||
<SetupProjectDialog />
|
||||
</div>
|
||||
@@ -36,10 +33,7 @@ export const ProjectsPage: FC = () => {
|
||||
fallback={
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-muted-foreground">
|
||||
<Trans
|
||||
id="projects.page.loading"
|
||||
message="Loading projects..."
|
||||
/>
|
||||
<Trans id="projects.page.loading" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -39,23 +39,15 @@ export const GlobalSidebar: FC<GlobalSidebarProps> = ({
|
||||
const settingsTab: SidebarTab = {
|
||||
id: "settings",
|
||||
icon: SettingsIcon,
|
||||
title: (
|
||||
<Trans
|
||||
id="settings.tab.title"
|
||||
message="Settings for display and notifications"
|
||||
/>
|
||||
),
|
||||
title: <Trans id="settings.tab.title" />,
|
||||
content: (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="border-b border-sidebar-border p-4">
|
||||
<h2 className="font-semibold text-lg">
|
||||
<Trans id="settings.title" message="Settings" />
|
||||
<Trans id="settings.title" />
|
||||
</h2>
|
||||
<p className="text-xs text-sidebar-foreground/70">
|
||||
<Trans
|
||||
id="settings.description"
|
||||
message="Display and behavior preferences"
|
||||
/>
|
||||
<Trans id="settings.description" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -63,7 +55,7 @@ export const GlobalSidebar: FC<GlobalSidebarProps> = ({
|
||||
fallback={
|
||||
<div className="flex-1 flex items-center justify-center p-4">
|
||||
<div className="text-sm text-sidebar-foreground/70">
|
||||
<Trans id="settings.loading" message="Loading settings..." />
|
||||
<Trans id="settings.loading" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -71,20 +63,14 @@ export const GlobalSidebar: FC<GlobalSidebarProps> = ({
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium text-sm text-sidebar-foreground">
|
||||
<Trans
|
||||
id="settings.section.session_display"
|
||||
message="Session Display"
|
||||
/>
|
||||
<Trans id="settings.section.session_display" />
|
||||
</h3>
|
||||
<SettingsControls openingProjectId={projectId ?? ""} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium text-sm text-sidebar-foreground">
|
||||
<Trans
|
||||
id="settings.section.notifications"
|
||||
message="Notifications"
|
||||
/>
|
||||
<Trans id="settings.section.notifications" />
|
||||
</h3>
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
@@ -97,9 +83,7 @@ export const GlobalSidebar: FC<GlobalSidebarProps> = ({
|
||||
const systemInfoTab: SidebarTab = {
|
||||
id: "system-info",
|
||||
icon: InfoIcon,
|
||||
title: (
|
||||
<Trans id="settings.section.system_info" message="System Information" />
|
||||
),
|
||||
title: <Trans id="settings.section.system_info" />,
|
||||
content: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SystemInfoCard />
|
||||
|
||||
@@ -16,13 +16,8 @@ interface NotFoundProps {
|
||||
}
|
||||
|
||||
export const NotFound: FC<NotFoundProps> = ({
|
||||
message = <Trans id="notfound.default.title" message="Page Not Found" />,
|
||||
description = (
|
||||
<Trans
|
||||
id="notfound.default.description"
|
||||
message="The page you are looking for does not exist or has been moved."
|
||||
/>
|
||||
),
|
||||
message = <Trans id="notfound.default.title" />,
|
||||
description = <Trans id="notfound.default.description" />,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
@@ -45,7 +40,7 @@ export const NotFound: FC<NotFoundProps> = ({
|
||||
variant="default"
|
||||
>
|
||||
<Home />
|
||||
<Trans id="notfound.button.go_home" message="Go to Home" />
|
||||
<Trans id="notfound.button.go_home" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -87,17 +87,14 @@ export const NotificationSettings: FC<NotificationSettingsProps> = ({
|
||||
onClick={handleTestSound}
|
||||
className="px-3"
|
||||
>
|
||||
<Trans id="notification.test" message="Test" />
|
||||
<Trans id="notification.test" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDescriptions && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans
|
||||
id="notification.description"
|
||||
message="Select a sound to play when a task completes"
|
||||
/>
|
||||
<Trans id="notification.description" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -121,19 +121,13 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
htmlFor={checkboxId}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans
|
||||
id="settings.session.hide_no_user_message"
|
||||
message="Hide sessions without user messages"
|
||||
/>
|
||||
<Trans id="settings.session.hide_no_user_message" />
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
{showDescriptions && (
|
||||
<p className="text-xs text-muted-foreground mt-1 ml-6">
|
||||
<Trans
|
||||
id="settings.session.hide_no_user_message.description"
|
||||
message="Only show sessions that contain user commands or messages"
|
||||
/>
|
||||
<Trans id="settings.session.hide_no_user_message.description" />
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -148,19 +142,13 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
htmlFor={`${checkboxId}-unify`}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans
|
||||
id="settings.session.unify_same_title"
|
||||
message="Unify sessions with same title"
|
||||
/>
|
||||
<Trans id="settings.session.unify_same_title" />
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
{showDescriptions && (
|
||||
<p className="text-xs text-muted-foreground mt-1 ml-6">
|
||||
<Trans
|
||||
id="settings.session.unify_same_title.description"
|
||||
message="Show only the latest session when multiple sessions have the same title"
|
||||
/>
|
||||
<Trans id="settings.session.unify_same_title.description" />
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -170,10 +158,7 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
htmlFor={enterKeyBehaviorId}
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
<Trans
|
||||
id="settings.input.enter_key_behavior"
|
||||
message="Enter Key Behavior"
|
||||
/>
|
||||
<Trans id="settings.input.enter_key_behavior" />
|
||||
</label>
|
||||
)}
|
||||
<Select
|
||||
@@ -185,31 +170,19 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="shift-enter-send">
|
||||
<Trans
|
||||
id="settings.input.enter_key_behavior.shift_enter"
|
||||
message="Shift+Enter to send (default)"
|
||||
/>
|
||||
<Trans id="settings.input.enter_key_behavior.shift_enter" />
|
||||
</SelectItem>
|
||||
<SelectItem value="enter-send">
|
||||
<Trans
|
||||
id="settings.input.enter_key_behavior.enter"
|
||||
message="Enter to send"
|
||||
/>
|
||||
<Trans id="settings.input.enter_key_behavior.enter" />
|
||||
</SelectItem>
|
||||
<SelectItem value="command-enter-send">
|
||||
<Trans
|
||||
id="settings.input.enter_key_behavior.command_enter"
|
||||
message="Command+Enter to send"
|
||||
/>
|
||||
<Trans id="settings.input.enter_key_behavior.command_enter" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{showDescriptions && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<Trans
|
||||
id="settings.input.enter_key_behavior.description"
|
||||
message="Choose how the Enter key behaves in message input"
|
||||
/>
|
||||
<Trans id="settings.input.enter_key_behavior.description" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -220,7 +193,7 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
htmlFor={permissionModeId}
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
<Trans id="settings.permission.mode" message="Permission Mode" />
|
||||
<Trans id="settings.permission.mode" />
|
||||
</label>
|
||||
)}
|
||||
<Select
|
||||
@@ -233,45 +206,27 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">
|
||||
<Trans
|
||||
id="settings.permission.mode.default"
|
||||
message="Default (Ask permission)"
|
||||
/>
|
||||
<Trans id="settings.permission.mode.default" />
|
||||
</SelectItem>
|
||||
<SelectItem value="acceptEdits">
|
||||
<Trans
|
||||
id="settings.permission.mode.accept_edits"
|
||||
message="Accept Edits (Auto-approve file edits)"
|
||||
/>
|
||||
<Trans id="settings.permission.mode.accept_edits" />
|
||||
</SelectItem>
|
||||
<SelectItem value="bypassPermissions">
|
||||
<Trans
|
||||
id="settings.permission.mode.bypass_permissions"
|
||||
message="Bypass Permissions (No prompts)"
|
||||
/>
|
||||
<Trans id="settings.permission.mode.bypass_permissions" />
|
||||
</SelectItem>
|
||||
<SelectItem value="plan">
|
||||
<Trans
|
||||
id="settings.permission.mode.plan"
|
||||
message="Plan Mode (Planning only)"
|
||||
/>
|
||||
<Trans id="settings.permission.mode.plan" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{showDescriptions && isToolApprovalAvailable && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<Trans
|
||||
id="settings.permission.mode.description"
|
||||
message="Control how Claude Code handles permission requests for file operations"
|
||||
/>
|
||||
<Trans id="settings.permission.mode.description" />
|
||||
</p>
|
||||
)}
|
||||
{showDescriptions && !isToolApprovalAvailable && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
<Trans
|
||||
id="settings.permission.mode.unavailable"
|
||||
message="This feature is not available in your Claude Code version. All tools will be automatically approved."
|
||||
/>
|
||||
<Trans id="settings.permission.mode.unavailable" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -282,7 +237,7 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
htmlFor={localeId}
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
<Trans id="settings.locale" message="Language" />
|
||||
<Trans id="settings.locale" />
|
||||
</label>
|
||||
)}
|
||||
<Select
|
||||
@@ -294,19 +249,16 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ja">
|
||||
<Trans id="settings.locale.ja" message="日本語" />
|
||||
<Trans id="settings.locale.ja" />
|
||||
</SelectItem>
|
||||
<SelectItem value="en">
|
||||
<Trans id="settings.locale.en" message="English" />
|
||||
<Trans id="settings.locale.en" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{showDescriptions && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<Trans
|
||||
id="settings.locale.description"
|
||||
message="Choose your preferred language"
|
||||
/>
|
||||
<Trans id="settings.locale.description" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -314,7 +266,7 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
<div className="space-y-2">
|
||||
{showLabels && (
|
||||
<label htmlFor={themeId} className="text-sm font-medium leading-none">
|
||||
<Trans id="settings.theme" message="Theme" />
|
||||
<Trans id="settings.theme" />
|
||||
</label>
|
||||
)}
|
||||
<Select value={theme ?? "system"} onValueChange={handleThemeChange}>
|
||||
@@ -323,22 +275,19 @@ export const SettingsControls: FC<SettingsControlsProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">
|
||||
<Trans id="settings.theme.light" message="Light" />
|
||||
<Trans id="settings.theme.light" />
|
||||
</SelectItem>
|
||||
<SelectItem value="dark">
|
||||
<Trans id="settings.theme.dark" message="Dark" />
|
||||
<Trans id="settings.theme.dark" />
|
||||
</SelectItem>
|
||||
<SelectItem value="system">
|
||||
<Trans id="settings.theme.system" message="System" />
|
||||
<Trans id="settings.theme.system" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{showDescriptions && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<Trans
|
||||
id="settings.theme.description"
|
||||
message="Choose your preferred color theme"
|
||||
/>
|
||||
<Trans id="settings.theme.description" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -26,43 +26,20 @@ const getFeatureInfo = (featureName: string): FeatureInfo => {
|
||||
switch (featureName) {
|
||||
case "tool-approval":
|
||||
return {
|
||||
title: (
|
||||
<Trans
|
||||
id="system_info.feature.tool_approval.title"
|
||||
message="Tool Execution Approval"
|
||||
/>
|
||||
),
|
||||
title: <Trans id="system_info.feature.tool_approval.title" />,
|
||||
description: (
|
||||
<Trans
|
||||
id="system_info.feature.tool_approval.description"
|
||||
message="Allows you to approve or reject tool executions before Claude runs them, giving you full control over actions"
|
||||
/>
|
||||
<Trans id="system_info.feature.tool_approval.description" />
|
||||
),
|
||||
};
|
||||
case "agent-sdk":
|
||||
return {
|
||||
title: (
|
||||
<Trans
|
||||
id="system_info.feature.agent_sdk.title"
|
||||
message="Enhanced Agent Mode"
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<Trans
|
||||
id="system_info.feature.agent_sdk.description"
|
||||
message="Uses @anthropic-ai/claude-agent-sdk instead of @anthropic-ai/claude-code for enhanced capabilities"
|
||||
/>
|
||||
),
|
||||
title: <Trans id="system_info.feature.agent_sdk.title" />,
|
||||
description: <Trans id="system_info.feature.agent_sdk.description" />,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: featureName,
|
||||
description: (
|
||||
<Trans
|
||||
id="system_info.feature.unknown.description"
|
||||
message="Feature information not available"
|
||||
/>
|
||||
),
|
||||
description: <Trans id="system_info.feature.unknown.description" />,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -84,13 +61,10 @@ export const SystemInfoCard: FC = () => {
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="border-b border-sidebar-border p-4">
|
||||
<h2 className="font-semibold text-lg">
|
||||
<Trans id="system_info.title" message="System Information" />
|
||||
<Trans id="system_info.title" />
|
||||
</h2>
|
||||
<p className="text-xs text-sidebar-foreground/70">
|
||||
<Trans
|
||||
id="system_info.description"
|
||||
message="Version and feature information"
|
||||
/>
|
||||
<Trans id="system_info.description" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -98,14 +72,11 @@ export const SystemInfoCard: FC = () => {
|
||||
{/* Claude Code Viewer Version */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-sm text-sidebar-foreground">
|
||||
<Trans
|
||||
id="system_info.viewer_version"
|
||||
message="Claude Code Viewer"
|
||||
/>
|
||||
<Trans id="system_info.viewer_version" />
|
||||
</h3>
|
||||
<div className="flex justify-between items-center pl-2">
|
||||
<span className="text-xs text-sidebar-foreground/70">
|
||||
<Trans id="system_info.version_label" message="Version" />
|
||||
<Trans id="system_info.version_label" />
|
||||
</span>
|
||||
<Badge variant="secondary" className="text-xs font-mono">
|
||||
v{versionData?.version || "Unknown"}
|
||||
@@ -116,17 +87,17 @@ export const SystemInfoCard: FC = () => {
|
||||
{/* Claude Code Information */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-sm text-sidebar-foreground">
|
||||
<Trans id="system_info.claude_code" message="Claude Code" />
|
||||
<Trans id="system_info.claude_code" />
|
||||
</h3>
|
||||
<div className="space-y-2 pl-2">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-sidebar-foreground/70">
|
||||
<Trans id="system_info.executable_path" message="Executable" />
|
||||
<Trans id="system_info.executable_path" />
|
||||
</div>
|
||||
<div className="text-xs text-sidebar-foreground font-mono break-all">
|
||||
{claudeCodeMetaData?.executablePath || (
|
||||
<span className="text-sidebar-foreground/50">
|
||||
<Trans id="system_info.unknown" message="Unknown" />
|
||||
<Trans id="system_info.unknown" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -134,11 +105,11 @@ export const SystemInfoCard: FC = () => {
|
||||
|
||||
<div className="flex justify-between items-center pt-1">
|
||||
<span className="text-xs text-sidebar-foreground/70">
|
||||
<Trans id="system_info.version_label" message="Version" />
|
||||
<Trans id="system_info.version_label" />
|
||||
</span>
|
||||
<Badge variant="secondary" className="text-xs font-mono">
|
||||
{claudeCodeMetaData?.version || (
|
||||
<Trans id="system_info.unknown" message="Unknown" />
|
||||
<Trans id="system_info.unknown" />
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -150,10 +121,7 @@ export const SystemInfoCard: FC = () => {
|
||||
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between group">
|
||||
<h3 className="font-medium text-sm text-sidebar-foreground">
|
||||
<Trans
|
||||
id="system_info.available_features"
|
||||
message="Available Features"
|
||||
/>
|
||||
<Trans id="system_info.available_features" />
|
||||
</h3>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 text-sidebar-foreground/70 group-hover:text-sidebar-foreground transition-colors" />
|
||||
|
||||
48
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
@@ -100,11 +100,11 @@ export function getSoundDisplayName(
|
||||
soundType: NotificationSoundType,
|
||||
): ReactNode {
|
||||
const displayNames: Record<NotificationSoundType, ReactNode> = {
|
||||
none: <Trans id="notification.none" message="None" />,
|
||||
beep: <Trans id="notification.beep" message="Beep" />,
|
||||
chime: <Trans id="notification.chime" message="Chime" />,
|
||||
ping: <Trans id="notification.ping" message="Ping" />,
|
||||
pop: <Trans id="notification.pop" message="Pop" />,
|
||||
none: <Trans id="notification.none" />,
|
||||
beep: <Trans id="notification.beep" />,
|
||||
chime: <Trans id="notification.chime" />,
|
||||
ping: <Trans id="notification.ping" />,
|
||||
pop: <Trans id="notification.pop" />,
|
||||
};
|
||||
|
||||
return displayNames[soundType];
|
||||
|
||||
@@ -11,15 +11,8 @@ export const Route = createFileRoute("/projects/$projectId/latest/")({
|
||||
component: RouteComponent,
|
||||
notFoundComponent: () => (
|
||||
<NotFound
|
||||
message={
|
||||
<Trans id="notfound.project.title" message="Project Not Found" />
|
||||
}
|
||||
description={
|
||||
<Trans
|
||||
id="notfound.project.description"
|
||||
message="The project you are looking for does not exist."
|
||||
/>
|
||||
}
|
||||
message={<Trans id="notfound.project.title" />}
|
||||
description={<Trans id="notfound.project.description" />}
|
||||
/>
|
||||
),
|
||||
loader: async ({ params }) => {
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import { Trans } from "@lingui/react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { z } from "zod";
|
||||
import { useProject } from "../../../../../app/projects/[projectId]/hooks/useProject";
|
||||
import { SessionPageContent } from "../../../../../app/projects/[projectId]/sessions/[sessionId]/components/SessionPageContent";
|
||||
import { tabSchema } from "../../../../../app/projects/[projectId]/sessions/[sessionId]/components/sessionSidebar/schema";
|
||||
import { NotFound } from "../../../../../components/NotFound";
|
||||
|
||||
const sessionSearchSchema = z.object({
|
||||
tab: tabSchema.optional().default("sessions"),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/projects/$projectId/sessions/$sessionId/",
|
||||
)({
|
||||
validateSearch: sessionSearchSchema,
|
||||
component: RouteComponent,
|
||||
notFoundComponent: () => (
|
||||
<NotFound
|
||||
message={
|
||||
<Trans id="notfound.session.title" message="Session Not Found" />
|
||||
}
|
||||
description={
|
||||
<Trans
|
||||
id="notfound.session.description"
|
||||
message="The session you are looking for does not exist."
|
||||
/>
|
||||
}
|
||||
message={<Trans id="notfound.session.title" />}
|
||||
description={<Trans id="notfound.session.description" />}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const params = Route.useParams();
|
||||
const search = Route.useSearch();
|
||||
const { data } = useProject(params.projectId);
|
||||
const projectName = data.pages[0]?.project.meta.projectName;
|
||||
|
||||
@@ -41,6 +42,7 @@ function RouteComponent() {
|
||||
<SessionPageContent
|
||||
projectId={params.projectId}
|
||||
sessionId={params.sessionId}
|
||||
tab={search.tab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||