chore: Improve session detail UI (#49)

This commit is contained in:
きむそん
2025-11-02 21:39:33 +09:00
committed by GitHub
parent 6c93fe58b0
commit c9d5dd14a6
96 changed files with 4968 additions and 4691 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -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
View File

@@ -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)

View File

@@ -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>
</>
) : (

View File

@@ -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,

View File

@@ -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"

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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

View File

@@ -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}
/>
</>
);
};

View File

@@ -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" ? (

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
}

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />

View 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 };

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -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];

View File

@@ -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 }) => {

View File

@@ -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}
/>
</>
);