kcg-code 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +84 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +147 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +114 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +101 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +327 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +362 -0
- package/src/lib/skills-catalog.ts +138 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +427 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat homepage: headline acak + logo, lalu composer. Di bawah kotak input
|
|
3
|
+
* ada baris polos (tanpa border): pemilih Project di kiri, agent + model di
|
|
4
|
+
* kanan (satu trigger `ModelPicker` — model bergantung pada agent).
|
|
5
|
+
*
|
|
6
|
+
* Tata letak: semuanya di tengah layar. Desktop menampilkan daftar Session
|
|
7
|
+
* terbaru (`HomeRecentSessions`) di bawah composer; di HP daftar itu
|
|
8
|
+
* disembunyikan dan logo ditumpuk di atas headline.
|
|
9
|
+
*
|
|
10
|
+
* Kirim = buat Session baru di Project terpilih (`POST /api/sessions`),
|
|
11
|
+
* titipkan teks sebagai prompt tertunda (`lib/pending-prompt.ts`), lalu
|
|
12
|
+
* pindah ke Session view — `useSessionChat` mengirimnya sekali setelah
|
|
13
|
+
* koneksi WS ter-attach. Project terakhir dipakai diingat di localStorage.
|
|
14
|
+
*
|
|
15
|
+
* Mode agent ada di dalam kotak input, kiri bawah (seperti composer Session).
|
|
16
|
+
* Pemilih model & agent sama dengan Session (`ModelPicker`, `AgentPicker`) —
|
|
17
|
+
* tanpa Session, pilihan dikirim di body `POST /api/sessions`.
|
|
18
|
+
* Keduanya di-reset saat Project berganti karena daftarnya per Project.
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
CheckIcon,
|
|
22
|
+
ChevronDownIcon,
|
|
23
|
+
FolderIcon,
|
|
24
|
+
FolderPlusIcon,
|
|
25
|
+
SendHorizontalIcon,
|
|
26
|
+
} from "lucide-react";
|
|
27
|
+
import { useEffect, useRef, useState } from "react";
|
|
28
|
+
import { HomeRecentSessions } from "@/components/projects/HomeRecentSessions";
|
|
29
|
+
import { AgentPicker } from "@/components/sessions/AgentPicker";
|
|
30
|
+
import { ModelPicker } from "@/components/sessions/ModelPicker";
|
|
31
|
+
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
32
|
+
import { Button } from "@/components/ui/button";
|
|
33
|
+
import {
|
|
34
|
+
DropdownMenu,
|
|
35
|
+
DropdownMenuContent,
|
|
36
|
+
DropdownMenuGroup,
|
|
37
|
+
DropdownMenuItem,
|
|
38
|
+
DropdownMenuLabel,
|
|
39
|
+
DropdownMenuSeparator,
|
|
40
|
+
DropdownMenuTrigger,
|
|
41
|
+
} from "@/components/ui/dropdown-menu";
|
|
42
|
+
import {
|
|
43
|
+
InputGroup,
|
|
44
|
+
InputGroupAddon,
|
|
45
|
+
InputGroupButton,
|
|
46
|
+
InputGroupTextarea,
|
|
47
|
+
} from "@/components/ui/input-group";
|
|
48
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
49
|
+
import { useRouter } from "@/hooks/useRouter";
|
|
50
|
+
import { readPreferredAgentType, storePreferredAgentType } from "@/lib/agent-types";
|
|
51
|
+
import { ApiError, apiFetch } from "@/lib/api";
|
|
52
|
+
import { notifyDataChanged } from "@/lib/data-events";
|
|
53
|
+
import { pickHeadline } from "@/lib/headlines";
|
|
54
|
+
import { setPendingPrompt } from "@/lib/pending-prompt";
|
|
55
|
+
import type { ProjectOverview } from "@/lib/project-overview";
|
|
56
|
+
import { sessionPath } from "@/lib/routes";
|
|
57
|
+
import { seedSession } from "@/lib/session-handoff";
|
|
58
|
+
import logo from "@/logo.svg";
|
|
59
|
+
import type { AgentType, Session, SessionModel } from "@/types";
|
|
60
|
+
|
|
61
|
+
const LAST_PROJECT_KEY = "kcg-home-project";
|
|
62
|
+
|
|
63
|
+
export interface HomeChatProps {
|
|
64
|
+
/** Project terurut aktivitas terbaru (`buildProjectOverviews`). */
|
|
65
|
+
overviews: ProjectOverview[];
|
|
66
|
+
/** Buka dialog pembuatan Project baru. */
|
|
67
|
+
onNewProject: () => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readLastProject(): string | null {
|
|
71
|
+
return typeof localStorage === "undefined" ? null : localStorage.getItem(LAST_PROJECT_KEY);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function HomeChat({ overviews, onNewProject }: HomeChatProps) {
|
|
75
|
+
const { navigate } = useRouter();
|
|
76
|
+
// Dipilih sekali per kunjungan — tidak berganti saat re-render.
|
|
77
|
+
const [headline] = useState(() => pickHeadline());
|
|
78
|
+
const [text, setText] = useState("");
|
|
79
|
+
const [projectId, setProjectId] = useState<string | null>(readLastProject);
|
|
80
|
+
/** Backend Session baru (OpenCode / Claude Code), diingat per perangkat. */
|
|
81
|
+
const [agentType, setAgentType] = useState<AgentType>(readPreferredAgentType);
|
|
82
|
+
const [sending, setSending] = useState(false);
|
|
83
|
+
const [error, setError] = useState<string | null>(null);
|
|
84
|
+
/** Model & agent untuk Session baru; null = default opencode. */
|
|
85
|
+
const [model, setModel] = useState<SessionModel | null>(null);
|
|
86
|
+
const [agent, setAgent] = useState<string | null>(null);
|
|
87
|
+
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
88
|
+
|
|
89
|
+
// Project tersimpan sudah dihapus / belum ada -> pakai yang paling baru aktif.
|
|
90
|
+
const selectedOverview =
|
|
91
|
+
overviews.find((o) => o.project.id === projectId) ?? overviews[0] ?? null;
|
|
92
|
+
const selected = selectedOverview?.project ?? null;
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
textareaRef.current?.focus();
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const choose = (id: string) => {
|
|
99
|
+
if (id !== selected?.id) {
|
|
100
|
+
// Model/agent tersedia per Project (config opencode masing-masing).
|
|
101
|
+
setModel(null);
|
|
102
|
+
setAgent(null);
|
|
103
|
+
}
|
|
104
|
+
setProjectId(id);
|
|
105
|
+
if (typeof localStorage !== "undefined") localStorage.setItem(LAST_PROJECT_KEY, id);
|
|
106
|
+
requestAnimationFrame(() => textareaRef.current?.focus());
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const chooseAgentType = (next: AgentType) => {
|
|
110
|
+
if (next !== agentType) {
|
|
111
|
+
// Model & mode berasal dari backend masing-masing.
|
|
112
|
+
setModel(null);
|
|
113
|
+
setAgent(null);
|
|
114
|
+
}
|
|
115
|
+
setAgentType(next);
|
|
116
|
+
storePreferredAgentType(next);
|
|
117
|
+
// Tanpa fokus ke textarea: dipanggil dari dialog ModelPicker yang masih
|
|
118
|
+
// terbuka (fokus terkunci di dialog sampai ditutup).
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const canSend = selected !== null && text.trim() !== "" && !sending;
|
|
122
|
+
|
|
123
|
+
const submit = async (event?: React.FormEvent<HTMLFormElement>) => {
|
|
124
|
+
event?.preventDefault();
|
|
125
|
+
if (!canSend || selected === null) return;
|
|
126
|
+
setSending(true);
|
|
127
|
+
setError(null);
|
|
128
|
+
try {
|
|
129
|
+
const res = await apiFetch("/api/sessions", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body: JSON.stringify({ agentType, projectId: selected.id, model, agent }),
|
|
132
|
+
});
|
|
133
|
+
const { session } = (await res.json()) as { session: Session };
|
|
134
|
+
setPendingPrompt(session.id, text);
|
|
135
|
+
// Session view dirender langsung dari data ini (tanpa fetch ulang),
|
|
136
|
+
// lalu composer "berpindah" ke dasar layar lewat View Transition.
|
|
137
|
+
seedSession(session);
|
|
138
|
+
notifyDataChanged();
|
|
139
|
+
navigate(sessionPath(selected.id, session.id), { transition: "to-session" });
|
|
140
|
+
} catch (e) {
|
|
141
|
+
setError(e instanceof ApiError ? e.message : "Failed to start a new session");
|
|
142
|
+
setSending(false);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="mx-auto flex w-full max-w-2xl flex-1 flex-col justify-center gap-6 pb-[10dvh]">
|
|
148
|
+
{/* HP: logo di atas headline (tengah); desktop: berdampingan. */}
|
|
149
|
+
<div
|
|
150
|
+
data-vt-name="home-headline"
|
|
151
|
+
className="flex flex-col items-center justify-center gap-3 px-2 sm:flex-row"
|
|
152
|
+
>
|
|
153
|
+
<img src={logo} alt="" aria-hidden className="size-10 shrink-0 sm:size-12" />
|
|
154
|
+
<h1 className="text-balance text-center text-2xl font-semibold tracking-tight sm:text-left sm:text-3xl">
|
|
155
|
+
{headline}
|
|
156
|
+
</h1>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<form onSubmit={submit} className="flex flex-col gap-3">
|
|
160
|
+
{/* Container luar: hanya border (tanpa latar), membungkus dua section —
|
|
161
|
+
kotak input berlatar di atas, baris Project/model polos di bawah. */}
|
|
162
|
+
{/* `data-vt-name`: selama transisi "to-session" kotak ini dan area
|
|
163
|
+
Session view berbagi nama view-transition -> kotak composer
|
|
164
|
+
MELUAS menjadi halaman Session (container transform). */}
|
|
165
|
+
<div data-vt-name="session-surface" className="flex flex-col rounded-2xl border">
|
|
166
|
+
{/* Section 1 — kotak input: textarea + baris aksi (mode agent kiri,
|
|
167
|
+
kirim kanan), tata letak sama dengan composer Session. `-m-px`
|
|
168
|
+
menumpuk border kotak ini di atas border container luar. */}
|
|
169
|
+
<InputGroup className="-m-px w-auto rounded-2xl bg-card shadow-sm dark:bg-card">
|
|
170
|
+
<InputGroupTextarea
|
|
171
|
+
ref={textareaRef}
|
|
172
|
+
value={text}
|
|
173
|
+
onChange={(e) => setText(e.target.value)}
|
|
174
|
+
onKeyDown={(e) => {
|
|
175
|
+
// Enter kirim, Shift+Enter baris baru (abaikan saat komposisi IME).
|
|
176
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
void submit();
|
|
179
|
+
}
|
|
180
|
+
}}
|
|
181
|
+
placeholder={selected ? `Ask anything in ${selected.name}…` : "Pick a project first"}
|
|
182
|
+
aria-label="Message"
|
|
183
|
+
disabled={sending}
|
|
184
|
+
rows={2}
|
|
185
|
+
className="max-h-60 min-h-20 px-4 text-base leading-6 md:text-base"
|
|
186
|
+
/>
|
|
187
|
+
<InputGroupAddon align="block-end" className="gap-2 px-2 pb-2">
|
|
188
|
+
{selected && (
|
|
189
|
+
<AgentPicker
|
|
190
|
+
projectId={selected.id}
|
|
191
|
+
agentType={agentType}
|
|
192
|
+
sessionId={null}
|
|
193
|
+
agent={agent}
|
|
194
|
+
onChanged={setAgent}
|
|
195
|
+
disabled={sending}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
<InputGroupButton
|
|
199
|
+
type="submit"
|
|
200
|
+
size="icon-sm"
|
|
201
|
+
variant="default"
|
|
202
|
+
className="ml-auto size-9 rounded-full"
|
|
203
|
+
disabled={!canSend}
|
|
204
|
+
aria-label="Send"
|
|
205
|
+
>
|
|
206
|
+
{sending ? <Spinner /> : <SendHorizontalIcon />}
|
|
207
|
+
</InputGroupButton>
|
|
208
|
+
</InputGroupAddon>
|
|
209
|
+
</InputGroup>
|
|
210
|
+
|
|
211
|
+
{/* Section 2 — konteks Session baru: Project (kiri) & agent + model
|
|
212
|
+
(kanan), tanpa latar (hanya border container luar). */}
|
|
213
|
+
<div className="flex min-w-0 items-center justify-between gap-2 px-1.5 py-1">
|
|
214
|
+
<div className="flex min-w-0 items-center">
|
|
215
|
+
<DropdownMenu>
|
|
216
|
+
<DropdownMenuTrigger asChild>
|
|
217
|
+
<Button
|
|
218
|
+
type="button"
|
|
219
|
+
variant="ghost"
|
|
220
|
+
size="sm"
|
|
221
|
+
className="h-9 min-w-0 gap-1.5 px-2 font-normal text-muted-foreground hover:text-foreground"
|
|
222
|
+
aria-label={`Project: ${selected?.name ?? "none"}. Change project`}
|
|
223
|
+
disabled={sending}
|
|
224
|
+
>
|
|
225
|
+
<FolderIcon data-icon="inline-start" />
|
|
226
|
+
<span className="truncate">{selected?.name ?? "Select project"}</span>
|
|
227
|
+
<ChevronDownIcon data-icon="inline-end" />
|
|
228
|
+
</Button>
|
|
229
|
+
</DropdownMenuTrigger>
|
|
230
|
+
<DropdownMenuContent align="start" className="max-h-72 w-64">
|
|
231
|
+
<DropdownMenuLabel>Project</DropdownMenuLabel>
|
|
232
|
+
<DropdownMenuGroup>
|
|
233
|
+
{overviews.map(({ project }) => (
|
|
234
|
+
<DropdownMenuItem key={project.id} onSelect={() => choose(project.id)}>
|
|
235
|
+
<FolderIcon />
|
|
236
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
237
|
+
<span className="truncate">{project.name}</span>
|
|
238
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
239
|
+
{project.path}
|
|
240
|
+
</span>
|
|
241
|
+
</span>
|
|
242
|
+
{project.id === selected?.id && <CheckIcon />}
|
|
243
|
+
</DropdownMenuItem>
|
|
244
|
+
))}
|
|
245
|
+
</DropdownMenuGroup>
|
|
246
|
+
<DropdownMenuSeparator />
|
|
247
|
+
<DropdownMenuGroup>
|
|
248
|
+
<DropdownMenuItem onSelect={onNewProject}>
|
|
249
|
+
<FolderPlusIcon />
|
|
250
|
+
New project
|
|
251
|
+
</DropdownMenuItem>
|
|
252
|
+
</DropdownMenuGroup>
|
|
253
|
+
</DropdownMenuContent>
|
|
254
|
+
</DropdownMenu>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{selected && (
|
|
258
|
+
<div className="flex max-w-[60%] shrink-0 justify-end">
|
|
259
|
+
{/* Agent dipilih di dialog yang sama dengan model; trigger
|
|
260
|
+
menampilkan logo agent + nama model. */}
|
|
261
|
+
<ModelPicker
|
|
262
|
+
projectId={selected.id}
|
|
263
|
+
agentType={agentType}
|
|
264
|
+
sessionId={null}
|
|
265
|
+
model={model}
|
|
266
|
+
onChanged={setModel}
|
|
267
|
+
onAgentTypeChange={chooseAgentType}
|
|
268
|
+
variant="inline"
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{error && (
|
|
276
|
+
<Alert variant="destructive">
|
|
277
|
+
<AlertDescription>{error}</AlertDescription>
|
|
278
|
+
</Alert>
|
|
279
|
+
)}
|
|
280
|
+
</form>
|
|
281
|
+
|
|
282
|
+
{selectedOverview && (
|
|
283
|
+
<HomeRecentSessions overview={selectedOverview} className="hidden sm:mt-6 sm:flex" />
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daftar "lanjutkan" di homepage: beberapa Session terbaru milik Project yang
|
|
3
|
+
* sedang dipilih di composer (`HomeChat`). Ringkas — titik status, judul,
|
|
4
|
+
* logo agent, waktu relatif — plus tautan ke daftar Session lengkap.
|
|
5
|
+
*
|
|
6
|
+
* Datanya sudah ada di `ProjectOverview.recentSessions`, jadi tidak ada fetch.
|
|
7
|
+
*/
|
|
8
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
9
|
+
import { AgentLogo } from "@/components/sessions/AgentLogo";
|
|
10
|
+
import { useRouter } from "@/hooks/useRouter";
|
|
11
|
+
import { formatRelativeTime, type ProjectOverview } from "@/lib/project-overview";
|
|
12
|
+
import { projectPath, sessionPath } from "@/lib/routes";
|
|
13
|
+
import { SESSION_STATUS_DOT, SESSION_STATUS_LABEL } from "@/lib/session-status";
|
|
14
|
+
import { displaySessionTitle } from "@/lib/session-title";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
|
|
17
|
+
export interface HomeRecentSessionsProps {
|
|
18
|
+
overview: ProjectOverview;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function HomeRecentSessions({ overview, className }: HomeRecentSessionsProps) {
|
|
23
|
+
const { navigate } = useRouter();
|
|
24
|
+
const { project, recentSessions, sessionCount } = overview;
|
|
25
|
+
if (recentSessions.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
// Tautan biasa (bukan tombol) agar buka-di-tab-baru tetap berfungsi.
|
|
28
|
+
const go = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
|
|
29
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
navigate(href);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const allHref = projectPath(project.id);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<section
|
|
38
|
+
aria-label={`Recent sessions in ${project.name}`}
|
|
39
|
+
className={cn("flex flex-col", className)}
|
|
40
|
+
>
|
|
41
|
+
{/* Label + garis rambut: menandai section terpisah dari composer di
|
|
42
|
+
atasnya (bukan kelanjutan tray Project/model). */}
|
|
43
|
+
<div className="flex items-center gap-3 px-3 pb-2">
|
|
44
|
+
<h2 className="shrink-0 text-xs font-medium text-muted-foreground">Recent sessions</h2>
|
|
45
|
+
<span aria-hidden className="h-px flex-1 bg-border" />
|
|
46
|
+
{sessionCount > recentSessions.length && (
|
|
47
|
+
<a
|
|
48
|
+
href={allHref}
|
|
49
|
+
onClick={(e) => go(e, allHref)}
|
|
50
|
+
className="flex shrink-0 items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
|
51
|
+
>
|
|
52
|
+
View all {sessionCount}
|
|
53
|
+
<ArrowRightIcon className="size-3" />
|
|
54
|
+
</a>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
<ul className="flex flex-col">
|
|
58
|
+
{recentSessions.map((session) => {
|
|
59
|
+
const href = sessionPath(project.id, session.id);
|
|
60
|
+
const title = displaySessionTitle(session.title);
|
|
61
|
+
return (
|
|
62
|
+
<li key={session.id}>
|
|
63
|
+
<a
|
|
64
|
+
href={href}
|
|
65
|
+
onClick={(e) => go(e, href)}
|
|
66
|
+
aria-label={`Open session ${title}, ${SESSION_STATUS_LABEL[session.status]}`}
|
|
67
|
+
className="group flex min-h-10 items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"
|
|
68
|
+
>
|
|
69
|
+
<span
|
|
70
|
+
aria-hidden
|
|
71
|
+
className={cn("size-2 shrink-0 rounded-full", SESSION_STATUS_DOT[session.status])}
|
|
72
|
+
/>
|
|
73
|
+
<span className="min-w-0 flex-1 truncate text-sm text-muted-foreground group-hover:text-foreground group-focus-visible:text-foreground">
|
|
74
|
+
{title}
|
|
75
|
+
</span>
|
|
76
|
+
<AgentLogo
|
|
77
|
+
agentType={session.agentType}
|
|
78
|
+
className="size-3.5 text-muted-foreground"
|
|
79
|
+
/>
|
|
80
|
+
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">
|
|
81
|
+
{formatRelativeTime(session.updatedAt)}
|
|
82
|
+
</span>
|
|
83
|
+
</a>
|
|
84
|
+
</li>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</ul>
|
|
88
|
+
</section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialog multi-step untuk membuat Project baru (Requirement 10.2-10.4).
|
|
3
|
+
*
|
|
4
|
+
* Alur: Step 1 pilih direktori kerja via `FolderBrowser`, Step 2 beri nama
|
|
5
|
+
* project (pre-filled dari nama folder terpilih). Dipisah dari
|
|
6
|
+
* halaman daftar Project (`ProjectsPage`) agar logic wizard (step, reset
|
|
7
|
+
* state) tidak bercampur dengan logic daftar Project.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ArrowLeftIcon, ArrowRightIcon, FolderIcon, FolderPlusIcon } from "lucide-react";
|
|
11
|
+
import { useEffect, useState } from "react";
|
|
12
|
+
import { toast } from "sonner";
|
|
13
|
+
import { FolderBrowser } from "@/components/projects/FolderBrowser";
|
|
14
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
15
|
+
import { Button } from "@/components/ui/button";
|
|
16
|
+
import {
|
|
17
|
+
Dialog,
|
|
18
|
+
DialogContent,
|
|
19
|
+
DialogDescription,
|
|
20
|
+
DialogFooter,
|
|
21
|
+
DialogHeader,
|
|
22
|
+
DialogTitle,
|
|
23
|
+
} from "@/components/ui/dialog";
|
|
24
|
+
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
25
|
+
import { Input } from "@/components/ui/input";
|
|
26
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
27
|
+
import { useRouter } from "@/hooks/useRouter";
|
|
28
|
+
import { ApiError, apiFetch } from "@/lib/api";
|
|
29
|
+
import { settingsPath } from "@/lib/routes";
|
|
30
|
+
import { cn } from "@/lib/utils";
|
|
31
|
+
import type { WorkspaceInfo } from "@/server/services/workspace";
|
|
32
|
+
|
|
33
|
+
export interface NewProjectDialogProps {
|
|
34
|
+
open: boolean;
|
|
35
|
+
onOpenChange: (open: boolean) => void;
|
|
36
|
+
/** Dipanggil setelah Project berhasil dibuat, untuk memuat ulang daftar. */
|
|
37
|
+
onCreated: () => void | Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Step = 1 | 2;
|
|
41
|
+
|
|
42
|
+
/** Nama folder terakhir dari path relatif ("" bila root Sandbox). */
|
|
43
|
+
function basename(path: string): string {
|
|
44
|
+
const segments = path.split("/").filter((s) => s.length > 0);
|
|
45
|
+
return segments.at(-1) ?? "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function NewProjectDialog({ open, onOpenChange, onCreated }: NewProjectDialogProps) {
|
|
49
|
+
const [step, setStep] = useState<Step>(1);
|
|
50
|
+
const [path, setPath] = useState("");
|
|
51
|
+
const [name, setName] = useState("");
|
|
52
|
+
const [nameTouched, setNameTouched] = useState(false);
|
|
53
|
+
const [creating, setCreating] = useState(false);
|
|
54
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
55
|
+
/** Workspace (Sandbox_Root) aktif — ditampilkan di langkah 1. */
|
|
56
|
+
const [workspace, setWorkspace] = useState<WorkspaceInfo | null>(null);
|
|
57
|
+
const { navigate } = useRouter();
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!open) return;
|
|
61
|
+
let cancelled = false;
|
|
62
|
+
void apiFetch("/api/workspace")
|
|
63
|
+
.then((r) => r.json())
|
|
64
|
+
.then((b: { workspace: WorkspaceInfo }) => {
|
|
65
|
+
if (!cancelled) setWorkspace(b.workspace);
|
|
66
|
+
})
|
|
67
|
+
.catch(() => {
|
|
68
|
+
// Info tambahan saja; dialog tetap bisa dipakai.
|
|
69
|
+
});
|
|
70
|
+
return () => {
|
|
71
|
+
cancelled = true;
|
|
72
|
+
};
|
|
73
|
+
}, [open]);
|
|
74
|
+
|
|
75
|
+
const reset = () => {
|
|
76
|
+
setStep(1);
|
|
77
|
+
setPath("");
|
|
78
|
+
setName("");
|
|
79
|
+
setNameTouched(false);
|
|
80
|
+
setFormError(null);
|
|
81
|
+
setCreating(false);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleOpenChange = (next: boolean) => {
|
|
85
|
+
if (!next) reset();
|
|
86
|
+
onOpenChange(next);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const goToNaming = () => {
|
|
90
|
+
setFormError(null);
|
|
91
|
+
if (!nameTouched) setName(basename(path));
|
|
92
|
+
setStep(2);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const backToFolder = () => {
|
|
96
|
+
setFormError(null);
|
|
97
|
+
setStep(1);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const create = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
setCreating(true);
|
|
103
|
+
setFormError(null);
|
|
104
|
+
try {
|
|
105
|
+
await apiFetch("/api/projects", { method: "POST", body: JSON.stringify({ name, path }) });
|
|
106
|
+
await onCreated();
|
|
107
|
+
handleOpenChange(false);
|
|
108
|
+
toast.success(`Project "${name.trim()}" created.`);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
setFormError(e instanceof ApiError ? e.message : "Failed to create project");
|
|
111
|
+
} finally {
|
|
112
|
+
setCreating(false);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
118
|
+
<DialogContent className="flex h-[560px] max-h-[85vh] flex-col sm:max-w-lg">
|
|
119
|
+
<DialogHeader>
|
|
120
|
+
<div className="mt-4 flex flex-col gap-1.5">
|
|
121
|
+
<div className="flex items-center gap-1.5">
|
|
122
|
+
<span
|
|
123
|
+
className={cn(
|
|
124
|
+
"h-1.5 flex-1 rounded-full transition-colors",
|
|
125
|
+
step >= 1 ? "bg-primary" : "bg-muted",
|
|
126
|
+
)}
|
|
127
|
+
/>
|
|
128
|
+
<span
|
|
129
|
+
className={cn(
|
|
130
|
+
"h-1.5 flex-1 rounded-full transition-colors",
|
|
131
|
+
step >= 2 ? "bg-primary" : "bg-muted",
|
|
132
|
+
)}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
<span className="text-xs font-medium text-muted-foreground">Step {step} of 2</span>
|
|
136
|
+
</div>
|
|
137
|
+
<DialogTitle>{step === 1 ? "Choose working directory" : "Name your project"}</DialogTitle>
|
|
138
|
+
<DialogDescription>
|
|
139
|
+
{step === 1
|
|
140
|
+
? "Browse the Sandbox and pick the folder to use as the working directory."
|
|
141
|
+
: "This name identifies the project in the list."}
|
|
142
|
+
</DialogDescription>
|
|
143
|
+
</DialogHeader>
|
|
144
|
+
|
|
145
|
+
{step === 1 ? (
|
|
146
|
+
<>
|
|
147
|
+
<div className="min-h-0 flex-1">
|
|
148
|
+
<FolderBrowser path={path} onNavigate={setPath} />
|
|
149
|
+
</div>
|
|
150
|
+
{workspace && (
|
|
151
|
+
<p className="flex min-w-0 items-center gap-1.5 text-[12px] text-muted-foreground">
|
|
152
|
+
<span className="shrink-0">Projects live in</span>
|
|
153
|
+
<code className="min-w-0 truncate font-mono" title={workspace.root}>
|
|
154
|
+
{workspace.display}
|
|
155
|
+
</code>
|
|
156
|
+
<span aria-hidden>·</span>
|
|
157
|
+
<a
|
|
158
|
+
href={settingsPath("workspace")}
|
|
159
|
+
onClick={(e) => {
|
|
160
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
handleOpenChange(false);
|
|
163
|
+
navigate(settingsPath("workspace"));
|
|
164
|
+
}}
|
|
165
|
+
className="shrink-0 font-medium text-foreground underline underline-offset-2"
|
|
166
|
+
>
|
|
167
|
+
Change
|
|
168
|
+
</a>
|
|
169
|
+
</p>
|
|
170
|
+
)}
|
|
171
|
+
<DialogFooter>
|
|
172
|
+
<Button type="button" onClick={goToNaming}>
|
|
173
|
+
Next
|
|
174
|
+
<ArrowRightIcon data-icon="inline-end" />
|
|
175
|
+
</Button>
|
|
176
|
+
</DialogFooter>
|
|
177
|
+
</>
|
|
178
|
+
) : (
|
|
179
|
+
<form onSubmit={create} className="flex min-h-0 flex-1 flex-col gap-4">
|
|
180
|
+
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto">
|
|
181
|
+
<FieldGroup>
|
|
182
|
+
<Field>
|
|
183
|
+
<FieldLabel htmlFor="project-name">Project name</FieldLabel>
|
|
184
|
+
<Input
|
|
185
|
+
id="project-name"
|
|
186
|
+
value={name}
|
|
187
|
+
onChange={(e) => {
|
|
188
|
+
setName(e.target.value);
|
|
189
|
+
setNameTouched(true);
|
|
190
|
+
}}
|
|
191
|
+
placeholder="e.g. web-app"
|
|
192
|
+
maxLength={120}
|
|
193
|
+
autoFocus
|
|
194
|
+
/>
|
|
195
|
+
</Field>
|
|
196
|
+
<Field>
|
|
197
|
+
<FieldLabel>Working directory</FieldLabel>
|
|
198
|
+
<div className="flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm">
|
|
199
|
+
<FolderIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
200
|
+
<span className="min-w-0 flex-1 truncate font-mono text-muted-foreground">
|
|
201
|
+
{path === "" ? "/" : path}
|
|
202
|
+
</span>
|
|
203
|
+
<Button
|
|
204
|
+
type="button"
|
|
205
|
+
variant="ghost"
|
|
206
|
+
size="sm"
|
|
207
|
+
className="h-7 shrink-0 px-2"
|
|
208
|
+
onClick={backToFolder}
|
|
209
|
+
disabled={creating}
|
|
210
|
+
>
|
|
211
|
+
Change
|
|
212
|
+
</Button>
|
|
213
|
+
</div>
|
|
214
|
+
</Field>
|
|
215
|
+
</FieldGroup>
|
|
216
|
+
|
|
217
|
+
{formError && (
|
|
218
|
+
<Alert variant="destructive">
|
|
219
|
+
<AlertTitle>Failed to create project</AlertTitle>
|
|
220
|
+
<AlertDescription>{formError}</AlertDescription>
|
|
221
|
+
</Alert>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<DialogFooter>
|
|
226
|
+
<Button type="button" variant="ghost" onClick={backToFolder} disabled={creating}>
|
|
227
|
+
<ArrowLeftIcon data-icon="inline-start" />
|
|
228
|
+
Back
|
|
229
|
+
</Button>
|
|
230
|
+
<Button type="submit" disabled={creating || name.trim() === ""}>
|
|
231
|
+
{creating ? (
|
|
232
|
+
<>
|
|
233
|
+
<Spinner data-icon="inline-start" />
|
|
234
|
+
Creating…
|
|
235
|
+
</>
|
|
236
|
+
) : (
|
|
237
|
+
<>
|
|
238
|
+
<FolderPlusIcon data-icon="inline-start" />
|
|
239
|
+
Create project
|
|
240
|
+
</>
|
|
241
|
+
)}
|
|
242
|
+
</Button>
|
|
243
|
+
</DialogFooter>
|
|
244
|
+
</form>
|
|
245
|
+
)}
|
|
246
|
+
</DialogContent>
|
|
247
|
+
</Dialog>
|
|
248
|
+
);
|
|
249
|
+
}
|