chore: improve setup project feature

This commit is contained in:
d-kimsuon
2025-10-31 02:17:53 +09:00
parent d09797d7ed
commit 25f20f7f8d
12 changed files with 164 additions and 123 deletions

View File

@@ -1,19 +1,28 @@
import { Trans } from "@lingui/react";
import { useQuery } from "@tanstack/react-query";
import { ChevronRight, Folder } from "lucide-react";
import { type FC, useState } from "react";
import { Button } from "@/components/ui/button";
import { type FC, useEffect, useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { directoryListingQuery } from "@/lib/api/queries";
export type DirectoryPickerProps = {
selectedPath: string;
onPathChange: (path: string) => void;
};
export const DirectoryPicker: FC<DirectoryPickerProps> = ({ onPathChange }) => {
const [currentPath, setCurrentPath] = useState<string | undefined>(undefined);
const [showHidden, setShowHidden] = useState(false);
const { data, isLoading } = useQuery(directoryListingQuery(currentPath));
const { data, isLoading } = useQuery(
directoryListingQuery(currentPath, showHidden),
);
useEffect(() => {
if (data?.currentPath) {
onPathChange(data.currentPath);
}
}, [data?.currentPath, onPathChange]);
const handleNavigate = (entryPath: string) => {
if (entryPath === "") {
@@ -25,20 +34,26 @@ export const DirectoryPicker: FC<DirectoryPickerProps> = ({ onPathChange }) => {
setCurrentPath(newPath);
};
const handleSelect = () => {
onPathChange(data?.currentPath || "");
};
return (
<div className="border rounded-md">
<div className="p-3 border-b bg-muted/50 flex items-center justify-between">
<div className="p-3 border-b bg-muted/50">
<p className="text-sm font-medium">
<Trans id="directory_picker.current" message="Current:" />{" "}
<span className="font-mono">{data?.currentPath || "~"}</span>
</p>
<Button size="sm" onClick={handleSelect}>
<Trans id="directory_picker.select" message="Select This Directory" />
</Button>
</div>
<div className="p-3 border-b flex items-center gap-2">
<Checkbox
id="show-hidden"
checked={showHidden}
onCheckedChange={(checked) => setShowHidden(checked === true)}
/>
<Label htmlFor="show-hidden" className="text-sm cursor-pointer">
<Trans
id="directory_picker.show_hidden"
message="Show hidden files"
/>
</Label>
</div>
<div className="max-h-96 overflow-auto">
{isLoading ? (
@@ -49,6 +64,12 @@ export const DirectoryPicker: FC<DirectoryPickerProps> = ({ onPathChange }) => {
<div className="divide-y">
{data.entries
.filter((entry) => entry.type === "directory")
.filter(
(entry) =>
showHidden ||
entry.name === ".." ||
!entry.name.startsWith("."),
)
.map((entry) => (
<button
key={entry.path}

View File

@@ -17,26 +17,26 @@ import {
import { honoClient } from "@/lib/api/client";
import { DirectoryPicker } from "./DirectoryPicker";
export const CreateProjectDialog: FC = () => {
export const SetupProjectDialog: FC = () => {
const [open, setOpen] = useState(false);
const [selectedPath, setSelectedPath] = useState<string>("");
const navigate = useNavigate();
const createProjectMutation = useMutation({
const setupProjectMutation = useMutation({
mutationFn: async () => {
const response = await honoClient.api.projects.$post({
json: { projectPath: selectedPath },
});
if (!response.ok) {
throw new Error("Failed to create project");
throw new Error("Failed to set up project");
}
return await response.json();
},
onSuccess: (result) => {
toast.success("Project created successfully");
toast.success("Project set up successfully");
setOpen(false);
navigate({
to: "/projects/$projectId/sessions/$sessionId",
@@ -49,7 +49,7 @@ export const CreateProjectDialog: FC = () => {
onError: (error) => {
toast.error(
error instanceof Error ? error.message : "Failed to create project",
error instanceof Error ? error.message : "Failed to set up project",
);
},
});
@@ -65,12 +65,12 @@ export const CreateProjectDialog: FC = () => {
<DialogContent className="max-w-2xl" data-testid="new-project-modal">
<DialogHeader>
<DialogTitle>
<Trans id="project.create.title" message="Create New Project" />
<Trans id="project.setup.title" message="Setup New Project" />
</DialogTitle>
<DialogDescription>
<Trans
id="project.create.description"
message="Select a directory to initialize as a Claude Code project. This will run <0>/init</0> in the selected directory."
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" />,
}}
@@ -78,45 +78,26 @@ export const CreateProjectDialog: FC = () => {
</DialogDescription>
</DialogHeader>
<div className="py-4">
<DirectoryPicker
selectedPath={selectedPath}
onPathChange={setSelectedPath}
/>
{selectedPath ? (
<div className="mt-4 p-3 bg-muted rounded-md">
<p className="text-sm font-medium mb-1">
<Trans
id="project.create.selected_directory"
message="Selected directory:"
/>
</p>
<p className="text-sm text-muted-foreground font-mono">
{selectedPath}
</p>
</div>
) : null}
<DirectoryPicker onPathChange={setSelectedPath} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
<Trans id="common.action.cancel" message="Cancel" />
</Button>
<Button
onClick={async () => await createProjectMutation.mutateAsync()}
disabled={!selectedPath || createProjectMutation.isPending}
onClick={async () => await setupProjectMutation.mutateAsync()}
disabled={!selectedPath || setupProjectMutation.isPending}
>
{createProjectMutation.isPending ? (
{setupProjectMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
<Trans
id="project.create.action.creating"
message="Creating..."
id="project.setup.action.setting_up"
message="Setting up..."
/>
</>
) : (
<Trans
id="project.create.action.create"
message="Create Project"
/>
<Trans id="project.setup.action.setup" message="Setup Project" />
)}
</Button>
</DialogFooter>

View File

@@ -2,8 +2,8 @@ import { Trans } from "@lingui/react";
import { HistoryIcon } from "lucide-react";
import { type FC, Suspense } from "react";
import { GlobalSidebar } from "@/components/GlobalSidebar";
import { CreateProjectDialog } from "./components/CreateProjectDialog";
import { ProjectList } from "./components/ProjectList";
import { SetupProjectDialog } from "./components/SetupProjectDialog";
export const ProjectsPage: FC = () => {
return (
@@ -30,7 +30,7 @@ export const ProjectsPage: FC = () => {
<h2 className="text-xl font-semibold">
<Trans id="projects.page.title" message="Your Projects" />
</h2>
<CreateProjectDialog />
<SetupProjectDialog />
</div>
<Suspense
fallback={

View File

@@ -17,12 +17,20 @@ export const projectListQuery = {
},
} as const;
export const directoryListingQuery = (currentPath?: string) =>
export const directoryListingQuery = (
currentPath?: string,
showHidden?: boolean,
) =>
({
queryKey: ["directory-listing", currentPath],
queryKey: ["directory-listing", currentPath, showHidden],
queryFn: async (): Promise<DirectoryListingResult> => {
const response = await honoClient.api.fs["directory-browser"].$get({
query: currentPath ? { currentPath } : {},
query: {
...(currentPath ? { currentPath } : {}),
...(showHidden !== undefined
? { showHidden: showHidden.toString() }
: {}),
},
});
if (!response.ok) {

View File

@@ -517,27 +517,6 @@
],
"translation": "Conversation is paused..."
},
"project.create.title": {
"message": "Create New Project",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 64]],
"translation": "Create New Project"
},
"project.create.action.create": {
"message": "Create Project",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 112]],
"translation": "Create Project"
},
"project.create.action.creating": {
"message": "Creating...",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 106]],
"translation": "Creating..."
},
"cron_builder.cron_expression": {
"translation": "Cron Expression",
"message": "Cron Expression",
@@ -973,7 +952,7 @@
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/ProjectList.tsx", 36]],
"translation": "No Claude Code projects found in your ~/.claude/projects directory. Start a conversation with Claude Code to create your first project."
"translation": "No Claude Code projects found in your ~/.claude/projects directory. Start a conversation with Claude Code to set up your first project."
},
"directory_picker.no_directories": {
"message": "No directories found",
@@ -1207,13 +1186,6 @@
],
"translation": "Schema Validation Error"
},
"project.create.description": {
"message": "Select a directory to initialize as a Claude Code project. This will run <0>/init</0> in the selected directory.",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 67]],
"translation": "Select a directory to initialize as a Claude Code project. This will run <0>/init</0> in the selected directory."
},
"notification.description": {
"message": "Select a sound to play when a task completes",
"placeholders": {},
@@ -1252,12 +1224,12 @@
"origin": [["src/app/projects/components/DirectoryPicker.tsx", 42]],
"translation": "Select This Directory"
},
"project.create.selected_directory": {
"message": "Selected directory:",
"directory_picker.show_hidden": {
"message": "Show hidden files",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 84]],
"translation": "Selected directory:"
"origin": [["src/app/projects/components/DirectoryPicker.tsx", 56]],
"translation": "Show hidden files"
},
"chat.send": {
"message": "Send",
@@ -2217,5 +2189,40 @@
]
],
"translation": "Create"
},
"project.setup.title": {
"message": "Setup New Project",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 68]],
"translation": "Setup New Project"
},
"project.setup.action.setup": {
"message": "Setup Project",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 117]],
"translation": "Setup Project"
},
"project.setup.action.setting_up": {
"message": "Setting up...",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 111]],
"translation": "Setting up..."
},
"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.",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/SetupProjectDialog.tsx", 72]],
"translation": "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."
},
"project.setup.selected_directory": {
"message": "Selected directory:",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 89]],
"translation": "Selected directory:"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -517,27 +517,6 @@
],
"translation": "会話を一時停止中..."
},
"project.create.title": {
"message": "Create New Project",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 64]],
"translation": "新規プロジェクトを作成"
},
"project.create.action.create": {
"message": "Create Project",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 112]],
"translation": "プロジェクトを作成"
},
"project.create.action.creating": {
"message": "Creating...",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 106]],
"translation": "作成中..."
},
"cron_builder.cron_expression": {
"translation": "Cron式",
"message": "Cron Expression",
@@ -973,7 +952,7 @@
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/ProjectList.tsx", 36]],
"translation": "~/.claude/projectsディレクトリにClaude Codeプロジェクトが見つかりません。Claude Codeとの会話を開始して、最初のプロジェクトを作成してください。"
"translation": "~/.claude/projectsディレクトリにClaude Codeプロジェクトが見つかりません。Claude Codeとの会話を開始して、最初のプロジェクトをセットアップしてください。"
},
"directory_picker.no_directories": {
"message": "No directories found",
@@ -1207,13 +1186,6 @@
],
"translation": "スキーマ検証エラー"
},
"project.create.description": {
"message": "Select a directory to initialize as a Claude Code project. This will run <0>/init</0> in the selected directory.",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 67]],
"translation": "Claude Codeプロジェクトとして初期化するディレクトリを選択してください。選択したディレクトリで<0>/init</0>が実行されます。"
},
"notification.description": {
"message": "Select a sound to play when a task completes",
"placeholders": {},
@@ -1252,12 +1224,12 @@
"origin": [["src/app/projects/components/DirectoryPicker.tsx", 42]],
"translation": "このディレクトリを選択"
},
"project.create.selected_directory": {
"message": "Selected directory:",
"directory_picker.show_hidden": {
"message": "Show hidden files",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 84]],
"translation": "選択したディレクトリ:"
"origin": [["src/app/projects/components/DirectoryPicker.tsx", 56]],
"translation": "隠しファイルを表示"
},
"chat.send": {
"message": "Send",
@@ -2217,5 +2189,40 @@
]
],
"translation": "作成"
},
"project.setup.title": {
"message": "Setup New Project",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 68]],
"translation": "新規プロジェクトをセットアップ"
},
"project.setup.action.setup": {
"message": "Setup Project",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 117]],
"translation": "プロジェクトをセットアップ"
},
"project.setup.action.setting_up": {
"message": "Setting up...",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 111]],
"translation": "セットアップ中..."
},
"project.setup.description": {
"message": "Select 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.",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/SetupProjectDialog.tsx", 72]],
"translation": "Claude Codeプロジェクトとしてセットアップするディレクトリまで移動してください。CLAUDE.mdが存在する場合はプロジェクトを説明し、存在しない場合は<0>/init</0>で初期化します。"
},
"project.setup.selected_directory": {
"message": "Selected directory:",
"placeholders": {},
"comments": [],
"origin": [["src/app/projects/components/CreateProjectDialog.tsx", 89]],
"translation": "選択したディレクトリ:"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -17,6 +17,7 @@ export type DirectoryListingResult = {
export const getDirectoryListing = async (
rootPath: string,
basePath = "/",
showHidden = false,
): Promise<DirectoryListingResult> => {
const normalizedBasePath =
basePath === "/"
@@ -52,7 +53,7 @@ export const getDirectoryListing = async (
}
for (const dirent of dirents) {
if (dirent.name.startsWith(".")) {
if (!showHidden && dirent.name.startsWith(".")) {
continue;
}

View File

@@ -46,9 +46,10 @@ const LayerImpl = Effect.gen(function* () {
const getDirectoryListingRoute = (options: {
currentPath?: string | undefined;
showHidden?: boolean | undefined;
}) =>
Effect.promise(async () => {
const { currentPath } = options;
const { currentPath, showHidden = false } = options;
const rootPath = "/";
const defaultPath = homedir();
@@ -59,7 +60,11 @@ const LayerImpl = Effect.gen(function* () {
? targetPath.slice(rootPath.length)
: targetPath;
const result = await getDirectoryListing(rootPath, relativePath);
const result = await getDirectoryListing(
rootPath,
relativePath,
showHidden,
);
return {
response: result,

View File

@@ -1,3 +1,4 @@
import { FileSystem, Path } from "@effect/platform";
import { Context, Effect, Layer } from "effect";
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
import type { InferEffect } from "../../../lib/effect/types";
@@ -15,6 +16,8 @@ const LayerImpl = Effect.gen(function* () {
const userConfigService = yield* UserConfigService;
const sessionRepository = yield* SessionRepository;
const context = yield* ApplicationContext;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const getProjects = () =>
Effect.gen(function* () {
@@ -131,6 +134,10 @@ const LayerImpl = Effect.gen(function* () {
const projectId = encodeProjectId(claudeProjectFilePath);
const userConfig = yield* userConfigService.getUserConfig();
// Check if CLAUDE.md exists in the project directory
const claudeMdPath = path.join(projectPath, "CLAUDE.md");
const claudeMdExists = yield* fileSystem.exists(claudeMdPath);
const result = yield* claudeCodeLifeCycleService.startTask({
baseSession: {
cwd: projectPath,
@@ -139,7 +146,7 @@ const LayerImpl = Effect.gen(function* () {
},
userConfig,
input: {
text: "/init",
text: claudeMdExists ? "describe this project" : "/init",
},
});

View File

@@ -543,6 +543,10 @@ export const routes = (app: HonoAppType) =>
"query",
z.object({
currentPath: z.string().optional(),
showHidden: z
.string()
.optional()
.transform((val) => val === "true"),
}),
),
async (c) => {