kcg-code 0.0.0-stage → 0.1.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 +228 -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 +83 -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__/dashboard.test.ts +62 -0
- package/src/cli/args.ts +123 -0
- package/src/cli/commands.ts +174 -0
- package/src/cli/dashboard.tsx +343 -0
- package/src/cli/index.ts +37 -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 +142 -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 +250 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/PanelSection.tsx +102 -0
- package/src/components/projects/ProjectInstructions.tsx +229 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +448 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +123 -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 +72 -0
- package/src/components/sessions/ContextPanel.tsx +561 -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 +209 -0
- package/src/components/sessions/ModelSearchList.tsx +151 -0
- package/src/components/sessions/NewSessionDialog.tsx +189 -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 +640 -0
- package/src/components/sessions/SessionHeader.tsx +175 -0
- package/src/components/sessions/SessionList.tsx +396 -0
- package/src/components/sessions/SessionRow.tsx +137 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +236 -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 +220 -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 +262 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1226 -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 +265 -0
- package/src/components/skills/InstallNotifications.tsx +325 -0
- package/src/components/skills/InstallSkillDialog.tsx +233 -0
- package/src/components/skills/SkillCards.tsx +210 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -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/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 +85 -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 +97 -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 +234 -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 +52 -0
- package/src/lib/__tests__/project-overview.test.ts +173 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -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 +134 -0
- package/src/lib/__tests__/skills-catalog.test.ts +96 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -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 +74 -0
- package/src/lib/project-overview.ts +117 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +115 -0
- package/src/lib/session-handoff.ts +25 -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 +360 -0
- package/src/lib/skills-catalog.ts +99 -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 +130 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +299 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
- package/src/pages/skills/SkillsPage.tsx +701 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1031 -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 +415 -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 +157 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +117 -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__/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 +3469 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
- package/src/server/services/__tests__/skills-registry.test.ts +359 -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/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 +1374 -0
- package/src/server/services/skill-install-jobs.ts +262 -0
- package/src/server/services/skills-registry.ts +468 -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,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header chat ala aplikasi chat mobile: toggle sidebar — nama model (pembuka
|
|
3
|
+
* picker) — aksi. Nama model jadi judul karena itulah informasi yang paling
|
|
4
|
+
* sering dilihat & diganti; identitas Session (agentType + id) turun ke baris
|
|
5
|
+
* kedua yang hanya tampil di layar lebar.
|
|
6
|
+
*
|
|
7
|
+
* Tanpa tombol back: navigasi antar Session/Project lewat sidebar.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
MoonIcon,
|
|
11
|
+
MoreVerticalIcon,
|
|
12
|
+
PlayIcon,
|
|
13
|
+
SquareIcon,
|
|
14
|
+
SunIcon,
|
|
15
|
+
Trash2Icon,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { ModelPicker } from "@/components/sessions/ModelPicker";
|
|
19
|
+
import { Button } from "@/components/ui/button";
|
|
20
|
+
import {
|
|
21
|
+
DropdownMenu,
|
|
22
|
+
DropdownMenuContent,
|
|
23
|
+
DropdownMenuItem,
|
|
24
|
+
DropdownMenuSeparator,
|
|
25
|
+
DropdownMenuTrigger,
|
|
26
|
+
} from "@/components/ui/dropdown-menu";
|
|
27
|
+
import { SidebarTrigger } from "@/components/ui/sidebar";
|
|
28
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
29
|
+
import { useTheme } from "@/hooks/useTheme";
|
|
30
|
+
import type { WsConnectionStatus } from "@/hooks/useWebSocket";
|
|
31
|
+
import { SESSION_STATUS_DOT, SESSION_STATUS_LABEL, wsStatusLabel } from "@/lib/session-status";
|
|
32
|
+
import { cn } from "@/lib/utils";
|
|
33
|
+
import type { Session, SessionModel, SessionStatus } from "@/types";
|
|
34
|
+
|
|
35
|
+
export interface SessionHeaderProps {
|
|
36
|
+
session: Session;
|
|
37
|
+
status: SessionStatus;
|
|
38
|
+
wsStatus: WsConnectionStatus;
|
|
39
|
+
/** Aksi stop Session (engine) — status baru tiba via WS `session_status`. */
|
|
40
|
+
onStop: () => void;
|
|
41
|
+
/** Aksi start/resume Session (engine). */
|
|
42
|
+
onStart: () => void;
|
|
43
|
+
/** Buka dialog konfirmasi hapus Session (dirender di SessionView). */
|
|
44
|
+
onRequestDelete: () => void;
|
|
45
|
+
stopping: boolean;
|
|
46
|
+
starting: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function SessionHeader({
|
|
50
|
+
session,
|
|
51
|
+
status,
|
|
52
|
+
wsStatus,
|
|
53
|
+
onStop,
|
|
54
|
+
onStart,
|
|
55
|
+
onRequestDelete,
|
|
56
|
+
stopping,
|
|
57
|
+
starting,
|
|
58
|
+
}: SessionHeaderProps) {
|
|
59
|
+
const { theme, toggleTheme } = useTheme();
|
|
60
|
+
const isDark = theme === "dark";
|
|
61
|
+
/** Model pilihan Session — dapat diganti live; null = default opencode. */
|
|
62
|
+
const [model, setModel] = useState<SessionModel | null>(session.model);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<header className="shrink-0">
|
|
66
|
+
{/* Tanpa border — sidebar sudah jadi pembatas. Lebar penuh (bukan kolom
|
|
67
|
+
percakapan) agar toggle sidebar menempel di tepi kiri, sama seperti
|
|
68
|
+
header shell di halaman lain. Navigasi kembali lewat sidebar. */}
|
|
69
|
+
<div className="flex w-full items-center gap-1 px-3 py-3 sm:gap-2 sm:px-4">
|
|
70
|
+
{/* Buka/ciutkan sidebar Session (Sheet di HP). */}
|
|
71
|
+
<SidebarTrigger className="size-9 shrink-0" />
|
|
72
|
+
|
|
73
|
+
{/* Judul = nama model, sekaligus pembuka dialog pemilihan model.
|
|
74
|
+
Ikon chevron memberi tahu bahwa ini dapat diganti. */}
|
|
75
|
+
<div className="flex min-w-0 flex-1 flex-col items-start">
|
|
76
|
+
<ModelPicker
|
|
77
|
+
projectId={session.projectId}
|
|
78
|
+
sessionId={session.id}
|
|
79
|
+
model={model}
|
|
80
|
+
onChanged={setModel}
|
|
81
|
+
variant="heading"
|
|
82
|
+
/>
|
|
83
|
+
<span className="hidden truncate px-2 font-mono text-[11px] text-muted-foreground sm:block">
|
|
84
|
+
{session.agentType} · {session.id.slice(0, 8)}
|
|
85
|
+
{wsStatus !== "connected" && ` · ${wsStatusLabel(wsStatus)}`}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Status = teks biasa + titik (bukan pil yang terlihat seperti
|
|
90
|
+
tombol). Di HP cukup titiknya; label teks mulai dari sm. */}
|
|
91
|
+
<span
|
|
92
|
+
role="status"
|
|
93
|
+
className="flex shrink-0 items-center gap-1.5 px-1 text-xs font-medium text-muted-foreground"
|
|
94
|
+
title={`Session ${SESSION_STATUS_LABEL[status].toLowerCase()}`}
|
|
95
|
+
>
|
|
96
|
+
<span className="relative flex size-2" aria-hidden>
|
|
97
|
+
{status === "running" && (
|
|
98
|
+
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500/60 motion-reduce:hidden" />
|
|
99
|
+
)}
|
|
100
|
+
<span
|
|
101
|
+
className={cn("relative inline-flex size-2 rounded-full", SESSION_STATUS_DOT[status])}
|
|
102
|
+
/>
|
|
103
|
+
</span>
|
|
104
|
+
<span className="sr-only sm:not-sr-only">{SESSION_STATUS_LABEL[status]}</span>
|
|
105
|
+
</span>
|
|
106
|
+
|
|
107
|
+
{/* Stop/Start berlabel teks agar jelas fungsinya (bukan ikon kotak
|
|
108
|
+
tanpa arti). Di HP label disembunyikan, tapi tombol tetap punya
|
|
109
|
+
aria-label & ukuran sentuh 40px. Saat model sedang merespon,
|
|
110
|
+
penghentian balasan ada di composer. */}
|
|
111
|
+
{status === "running" ? (
|
|
112
|
+
<Button
|
|
113
|
+
type="button"
|
|
114
|
+
variant="outline"
|
|
115
|
+
size="sm"
|
|
116
|
+
onClick={onStop}
|
|
117
|
+
disabled={stopping}
|
|
118
|
+
aria-label="Stop session"
|
|
119
|
+
className="relative size-8 shrink-0 rounded-full px-0 after:absolute after:-inset-1 hover:border-destructive/40 hover:bg-destructive/10 hover:text-destructive sm:h-8 sm:w-auto sm:px-3 sm:after:hidden dark:hover:bg-destructive/15"
|
|
120
|
+
>
|
|
121
|
+
{stopping ? (
|
|
122
|
+
<Spinner data-icon="inline-start" />
|
|
123
|
+
) : (
|
|
124
|
+
<SquareIcon data-icon="inline-start" className="size-3.5 fill-current" />
|
|
125
|
+
)}
|
|
126
|
+
<span className="hidden sm:inline">{stopping ? "Stopping…" : "Stop"}</span>
|
|
127
|
+
</Button>
|
|
128
|
+
) : (
|
|
129
|
+
<Button
|
|
130
|
+
type="button"
|
|
131
|
+
size="sm"
|
|
132
|
+
onClick={onStart}
|
|
133
|
+
disabled={starting}
|
|
134
|
+
aria-label={status === "crashed" ? "Restart session" : "Start session"}
|
|
135
|
+
className="relative size-8 shrink-0 rounded-full px-0 after:absolute after:-inset-1 sm:h-8 sm:w-auto sm:px-3 sm:after:hidden"
|
|
136
|
+
>
|
|
137
|
+
{starting ? (
|
|
138
|
+
<Spinner data-icon="inline-start" />
|
|
139
|
+
) : (
|
|
140
|
+
<PlayIcon data-icon="inline-start" className="size-3.5 fill-current" />
|
|
141
|
+
)}
|
|
142
|
+
<span className="hidden sm:inline">
|
|
143
|
+
{starting ? "Starting…" : status === "crashed" ? "Restart" : "Start"}
|
|
144
|
+
</span>
|
|
145
|
+
</Button>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
<DropdownMenu>
|
|
149
|
+
<DropdownMenuTrigger asChild>
|
|
150
|
+
<Button
|
|
151
|
+
type="button"
|
|
152
|
+
variant="ghost"
|
|
153
|
+
size="icon"
|
|
154
|
+
aria-label="Session actions"
|
|
155
|
+
className="size-10 shrink-0 sm:size-9"
|
|
156
|
+
>
|
|
157
|
+
<MoreVerticalIcon />
|
|
158
|
+
</Button>
|
|
159
|
+
</DropdownMenuTrigger>
|
|
160
|
+
<DropdownMenuContent align="end">
|
|
161
|
+
<DropdownMenuItem onSelect={toggleTheme}>
|
|
162
|
+
{isDark ? <SunIcon /> : <MoonIcon />}
|
|
163
|
+
{isDark ? "Light mode" : "Dark mode"}
|
|
164
|
+
</DropdownMenuItem>
|
|
165
|
+
<DropdownMenuSeparator />
|
|
166
|
+
<DropdownMenuItem variant="destructive" onSelect={onRequestDelete}>
|
|
167
|
+
<Trash2Icon />
|
|
168
|
+
Delete session
|
|
169
|
+
</DropdownMenuItem>
|
|
170
|
+
</DropdownMenuContent>
|
|
171
|
+
</DropdownMenu>
|
|
172
|
+
</div>
|
|
173
|
+
</header>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daftar & pembuatan Session milik sebuah Project (Requirement 1.1, 1.3, 1.5).
|
|
3
|
+
*
|
|
4
|
+
* Halaman detail Project — sengaja minimal: tipografi & ruang kosong yang
|
|
5
|
+
* bekerja, bukan kartu/tile.
|
|
6
|
+
* - Header: nama Project besar, path (salin saat hover), satu baris ringkasan
|
|
7
|
+
* ("3 sessions · 1 running"). Aksi: "New session" + menu "..." Reload/Delete
|
|
8
|
+
* (`ProjectMenu`; hapus tetap lewat dialog konfirmasi). Desktop: di
|
|
9
|
+
* kanan judul. Layar sempit: "New session" jadi tombol melayang kanan-bawah
|
|
10
|
+
* (area jempol); toolbar di bawah judul berisi "Details" (panel samping)
|
|
11
|
+
* dan grup Reload/Delete.
|
|
12
|
+
* - Panel samping (`ProjectSidePanel`, gaya Claude Projects): custom
|
|
13
|
+
* instruction, MCP server, dan skill. Layar `lg`+ = kolom kanan sticky;
|
|
14
|
+
* lebih sempit = Sheet dari kanan lewat tombol di header.
|
|
15
|
+
* Tanpa tombol back — navigasi lewat sidebar.
|
|
16
|
+
* - Daftar Session: baris polos dipisah hairline, status lewat titik warna.
|
|
17
|
+
* Urutan dari `sortSessions` (running -> crashed -> stopped, terbaru dulu).
|
|
18
|
+
* - `GET /api/sessions` di-filter per `projectId` (Requirement 1.5).
|
|
19
|
+
*
|
|
20
|
+
* File ini hanya menyusun area dari bagian yang sudah dipisah:
|
|
21
|
+
* - State & aksi daftar: `hooks/useSessionList.ts`.
|
|
22
|
+
* - Form pembuatan Session: `NewSessionDialog.tsx`.
|
|
23
|
+
* - Satu baris Session: `SessionRow.tsx`; dialog konfirmasi hapus:
|
|
24
|
+
* `ConfirmSessionDeleteDialog.tsx` & `ConfirmDeleteProjectDialog.tsx`.
|
|
25
|
+
*/
|
|
26
|
+
import {
|
|
27
|
+
CheckIcon,
|
|
28
|
+
CopyIcon,
|
|
29
|
+
MessagesSquareIcon,
|
|
30
|
+
MoreHorizontalIcon,
|
|
31
|
+
PanelRightIcon,
|
|
32
|
+
PlusIcon,
|
|
33
|
+
RefreshCwIcon,
|
|
34
|
+
Trash2Icon,
|
|
35
|
+
} from "lucide-react";
|
|
36
|
+
import { useEffect, useState } from "react";
|
|
37
|
+
import { toast } from "sonner";
|
|
38
|
+
import { ProjectSidePanel } from "@/components/projects/ProjectSidePanel";
|
|
39
|
+
import { ConfirmDeleteProjectDialog } from "@/components/sessions/ConfirmDeleteProjectDialog";
|
|
40
|
+
import { ConfirmSessionDeleteDialog } from "@/components/sessions/ConfirmSessionDeleteDialog";
|
|
41
|
+
import { NewSessionDialog } from "@/components/sessions/NewSessionDialog";
|
|
42
|
+
import { SessionListSkeleton, SessionRow } from "@/components/sessions/SessionRow";
|
|
43
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
44
|
+
import { Button } from "@/components/ui/button";
|
|
45
|
+
import {
|
|
46
|
+
DropdownMenu,
|
|
47
|
+
DropdownMenuContent,
|
|
48
|
+
DropdownMenuItem,
|
|
49
|
+
DropdownMenuSeparator,
|
|
50
|
+
DropdownMenuTrigger,
|
|
51
|
+
} from "@/components/ui/dropdown-menu";
|
|
52
|
+
import {
|
|
53
|
+
Sheet,
|
|
54
|
+
SheetContent,
|
|
55
|
+
SheetDescription,
|
|
56
|
+
SheetHeader,
|
|
57
|
+
SheetTitle,
|
|
58
|
+
} from "@/components/ui/sheet";
|
|
59
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
60
|
+
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
|
61
|
+
import { useSessionList } from "@/hooks/useSessionList";
|
|
62
|
+
import { notifyDataChanged } from "@/lib/data-events";
|
|
63
|
+
import { describeSessionSummary } from "@/lib/session-summary";
|
|
64
|
+
import { cn } from "@/lib/utils";
|
|
65
|
+
import type { Project, Session } from "@/types";
|
|
66
|
+
|
|
67
|
+
/** Breakpoint `lg` Tailwind — di atasnya panel samping tampil inline. */
|
|
68
|
+
const WIDE_QUERY = "(min-width: 1024px)";
|
|
69
|
+
|
|
70
|
+
export interface SessionListProps {
|
|
71
|
+
project: Project;
|
|
72
|
+
onOpenSession: (session: Session) => void;
|
|
73
|
+
/** Dipanggil setelah Project dihapus, untuk kembali ke daftar Project. */
|
|
74
|
+
onDeleted?: () => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Path Project yang bisa diklik untuk disalin — ikon muncul saat hover. */
|
|
78
|
+
function CopyablePath({ path }: { path: string }) {
|
|
79
|
+
const [copied, setCopied] = useState(false);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!copied) return;
|
|
83
|
+
const t = setTimeout(() => setCopied(false), 1500);
|
|
84
|
+
return () => clearTimeout(t);
|
|
85
|
+
}, [copied]);
|
|
86
|
+
|
|
87
|
+
const copy = async () => {
|
|
88
|
+
try {
|
|
89
|
+
await navigator.clipboard.writeText(path);
|
|
90
|
+
setCopied(true);
|
|
91
|
+
toast.success("Path copied.");
|
|
92
|
+
} catch {
|
|
93
|
+
// Clipboard API butuh konteks aman (HTTPS / localhost).
|
|
94
|
+
toast.error("Couldn't copy. Select the path and copy it manually.");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() => void copy()}
|
|
102
|
+
title={path}
|
|
103
|
+
aria-label={copied ? "Path copied" : `Copy path ${path}`}
|
|
104
|
+
className="group/path -mx-1 flex max-w-full min-w-0 items-center gap-1.5 rounded px-1 font-mono text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"
|
|
105
|
+
>
|
|
106
|
+
<span className="truncate">{path}</span>
|
|
107
|
+
{copied ? (
|
|
108
|
+
<CheckIcon className="size-3 shrink-0 text-emerald-500" aria-hidden />
|
|
109
|
+
) : (
|
|
110
|
+
<CopyIcon
|
|
111
|
+
className="size-3 shrink-0 opacity-0 transition-opacity group-hover/path:opacity-100 group-focus-visible/path:opacity-100"
|
|
112
|
+
aria-hidden
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Aksi sekunder Project (Reload, Delete) di satu menu "..." — Delete jarang
|
|
121
|
+
* dipakai & berbahaya, jadi tidak perlu tombol sendiri yang selalu terlihat.
|
|
122
|
+
* Delete tetap lewat dialog konfirmasi.
|
|
123
|
+
*/
|
|
124
|
+
function ProjectMenu({
|
|
125
|
+
loading,
|
|
126
|
+
onReload,
|
|
127
|
+
onDelete,
|
|
128
|
+
className,
|
|
129
|
+
}: {
|
|
130
|
+
loading: boolean;
|
|
131
|
+
onReload: () => void;
|
|
132
|
+
onDelete: () => void;
|
|
133
|
+
className?: string;
|
|
134
|
+
}) {
|
|
135
|
+
return (
|
|
136
|
+
<DropdownMenu>
|
|
137
|
+
<Tooltip>
|
|
138
|
+
<TooltipTrigger asChild>
|
|
139
|
+
<DropdownMenuTrigger asChild>
|
|
140
|
+
<Button
|
|
141
|
+
type="button"
|
|
142
|
+
variant="outline"
|
|
143
|
+
size="icon"
|
|
144
|
+
aria-label="More project actions"
|
|
145
|
+
className={cn("rounded-full text-muted-foreground", className)}
|
|
146
|
+
>
|
|
147
|
+
<MoreHorizontalIcon />
|
|
148
|
+
</Button>
|
|
149
|
+
</DropdownMenuTrigger>
|
|
150
|
+
</TooltipTrigger>
|
|
151
|
+
<TooltipContent>More</TooltipContent>
|
|
152
|
+
</Tooltip>
|
|
153
|
+
<DropdownMenuContent align="end" className="min-w-44">
|
|
154
|
+
<DropdownMenuItem onSelect={onReload} disabled={loading}>
|
|
155
|
+
<RefreshCwIcon className={cn(loading && "animate-spin")} />
|
|
156
|
+
Reload sessions
|
|
157
|
+
</DropdownMenuItem>
|
|
158
|
+
<DropdownMenuSeparator />
|
|
159
|
+
<DropdownMenuItem variant="destructive" onSelect={onDelete}>
|
|
160
|
+
<Trash2Icon />
|
|
161
|
+
Delete project
|
|
162
|
+
</DropdownMenuItem>
|
|
163
|
+
</DropdownMenuContent>
|
|
164
|
+
</DropdownMenu>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Empty state saat Project belum punya Session: kartu bergaris putus-putus
|
|
170
|
+
* (ikon, judul, penjelasan singkat, satu aksi utama).
|
|
171
|
+
*/
|
|
172
|
+
function EmptySessions({ projectName, onStart }: { projectName: string; onStart: () => void }) {
|
|
173
|
+
return (
|
|
174
|
+
<div className="flex flex-col items-center gap-6 rounded-2xl border border-dashed bg-muted/20 px-6 py-12 text-center sm:py-16">
|
|
175
|
+
<span
|
|
176
|
+
className="flex size-14 items-center justify-center rounded-2xl bg-primary/10 text-primary"
|
|
177
|
+
aria-hidden
|
|
178
|
+
>
|
|
179
|
+
<MessagesSquareIcon className="size-7" />
|
|
180
|
+
</span>
|
|
181
|
+
|
|
182
|
+
<div className="flex max-w-sm flex-col gap-1.5">
|
|
183
|
+
<h2 className="text-lg font-semibold tracking-tight">Start your first session</h2>
|
|
184
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
185
|
+
A session is a conversation with the agent inside{" "}
|
|
186
|
+
<span className="font-medium text-foreground">{projectName}</span>. It can read and edit
|
|
187
|
+
files here for you.
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<Button type="button" onClick={onStart} className="rounded-full px-5">
|
|
192
|
+
<PlusIcon data-icon="inline-start" />
|
|
193
|
+
New session
|
|
194
|
+
</Button>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function SessionList({
|
|
200
|
+
project: initialProject,
|
|
201
|
+
onOpenSession,
|
|
202
|
+
onDeleted,
|
|
203
|
+
}: SessionListProps) {
|
|
204
|
+
/** Salinan lokal agar perubahan (mis. instruksi) langsung tampil. */
|
|
205
|
+
const [project, setProject] = useState(initialProject);
|
|
206
|
+
useEffect(() => setProject(initialProject), [initialProject]);
|
|
207
|
+
const list = useSessionList({ projectId: project.id, onDeleted });
|
|
208
|
+
/** Form pembuatan Session — dialog, dibuka lewat tombol. */
|
|
209
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
210
|
+
const openForm = () => setFormOpen(true);
|
|
211
|
+
|
|
212
|
+
const hasSessions = !list.loading && !list.loadError && list.ordered.length > 0;
|
|
213
|
+
/** Layar lebar: panel samping inline. Sempit: panel di Sheet (tombol header). */
|
|
214
|
+
const wide = useMediaQuery(WIDE_QUERY);
|
|
215
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
216
|
+
const sidePanel = <ProjectSidePanel project={project} onProjectChange={setProject} />;
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
// lg+: grid setinggi viewport; tiap kolom scroll sendiri (min-h-0).
|
|
220
|
+
<div className="grid grid-cols-1 gap-x-10 gap-y-8 pt-4 sm:pt-8 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_20rem] xl:grid-cols-[minmax(0,1fr)_22rem]">
|
|
221
|
+
<div className="flex min-w-0 flex-col gap-8 lg:min-h-0 lg:gap-0">
|
|
222
|
+
{/* Header tetap di atas; hanya daftar Session yang scroll (lg+). */}
|
|
223
|
+
<header className="flex shrink-0 flex-col gap-4 lg:pb-6">
|
|
224
|
+
<div className="flex items-start justify-between gap-4">
|
|
225
|
+
<div className="flex min-w-0 flex-col gap-1.5">
|
|
226
|
+
<h1 className="truncate text-2xl font-semibold tracking-tight sm:text-3xl">
|
|
227
|
+
{project.name}
|
|
228
|
+
</h1>
|
|
229
|
+
<CopyablePath path={project.path} />
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Desktop: aksi di kanan judul (panel detail sudah inline). */}
|
|
233
|
+
{wide && (
|
|
234
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
235
|
+
<ProjectMenu
|
|
236
|
+
loading={list.loading}
|
|
237
|
+
onReload={() => void list.refresh()}
|
|
238
|
+
onDelete={list.openProjectDelete}
|
|
239
|
+
/>
|
|
240
|
+
<Button type="button" onClick={openForm} className="rounded-full px-4">
|
|
241
|
+
<PlusIcon data-icon="inline-start" />
|
|
242
|
+
New session
|
|
243
|
+
</Button>
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Satu baris ringkasan — cukup teks, tanpa tile. Disembunyikan saat
|
|
249
|
+
kosong: empty state di bawah sudah menyampaikannya. */}
|
|
250
|
+
{!list.loading && !list.loadError && list.summary.total > 0 && (
|
|
251
|
+
<p className="text-sm text-muted-foreground">{describeSessionSummary(list.summary)}</p>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{/* Layar sempit: toolbar aksi sekunder di bawah judul. Aksi utama
|
|
255
|
+
(New session) jadi tombol melayang kanan-bawah — area jempol. */}
|
|
256
|
+
{!wide && (
|
|
257
|
+
<div className="flex items-center justify-between gap-2">
|
|
258
|
+
<Button
|
|
259
|
+
type="button"
|
|
260
|
+
variant="outline"
|
|
261
|
+
onClick={() => setDetailsOpen(true)}
|
|
262
|
+
aria-label="Project details: instructions, MCP servers, skills"
|
|
263
|
+
className="h-10 rounded-full px-3.5"
|
|
264
|
+
>
|
|
265
|
+
<PanelRightIcon data-icon="inline-start" />
|
|
266
|
+
Details
|
|
267
|
+
</Button>
|
|
268
|
+
<ProjectMenu
|
|
269
|
+
className="size-10"
|
|
270
|
+
loading={list.loading}
|
|
271
|
+
onReload={() => void list.refresh()}
|
|
272
|
+
onDelete={list.openProjectDelete}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</header>
|
|
277
|
+
|
|
278
|
+
<section
|
|
279
|
+
aria-label="Sessions"
|
|
280
|
+
// pb-24 (mobile): ruang agar baris terakhir tidak tertutup FAB.
|
|
281
|
+
className="-mx-3 px-3 pb-24 lg:min-h-0 lg:flex-1 lg:overflow-y-auto lg:overscroll-contain lg:pb-16"
|
|
282
|
+
>
|
|
283
|
+
{list.loading ? (
|
|
284
|
+
<SessionListSkeleton />
|
|
285
|
+
) : list.loadError ? (
|
|
286
|
+
<Alert variant="destructive">
|
|
287
|
+
<AlertTitle>Failed to load sessions</AlertTitle>
|
|
288
|
+
<AlertDescription className="flex flex-col items-start gap-3">
|
|
289
|
+
{list.loadError}
|
|
290
|
+
<Button
|
|
291
|
+
type="button"
|
|
292
|
+
variant="outline"
|
|
293
|
+
size="sm"
|
|
294
|
+
onClick={() => void list.refresh()}
|
|
295
|
+
>
|
|
296
|
+
<RefreshCwIcon data-icon="inline-start" />
|
|
297
|
+
Retry
|
|
298
|
+
</Button>
|
|
299
|
+
</AlertDescription>
|
|
300
|
+
</Alert>
|
|
301
|
+
) : hasSessions ? (
|
|
302
|
+
<ul className="-mx-3 flex flex-col">
|
|
303
|
+
{list.ordered.map((session) => (
|
|
304
|
+
<li key={session.id}>
|
|
305
|
+
<SessionRow
|
|
306
|
+
session={session}
|
|
307
|
+
busy={
|
|
308
|
+
list.stopping === session.id ||
|
|
309
|
+
list.starting === session.id ||
|
|
310
|
+
list.deleting === session.id
|
|
311
|
+
}
|
|
312
|
+
onOpen={() => onOpenSession(session)}
|
|
313
|
+
onStop={() => void list.stop(session.id)}
|
|
314
|
+
onStart={() => void list.start(session.id)}
|
|
315
|
+
onDelete={() => list.requestDelete(session)}
|
|
316
|
+
/>
|
|
317
|
+
</li>
|
|
318
|
+
))}
|
|
319
|
+
</ul>
|
|
320
|
+
) : (
|
|
321
|
+
<EmptySessions projectName={project.name} onStart={openForm} />
|
|
322
|
+
)}
|
|
323
|
+
</section>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
{/* Panel samping: Instructions, MCP, Skills (gaya Claude Projects). */}
|
|
327
|
+
{wide ? (
|
|
328
|
+
<aside
|
|
329
|
+
aria-label="Project details"
|
|
330
|
+
// Panel mandiri: padding sama di keempat sisi; scrollbar-gutter
|
|
331
|
+
// stable di kedua tepi agar scrollbar tidak menggeser isi ke kiri.
|
|
332
|
+
// Tanpa kotak pembungkus: tiap seksi sudah berupa kartu sendiri.
|
|
333
|
+
// Scroll mandiri; pr kecil agar scrollbar tidak menempel ke kartu.
|
|
334
|
+
className="-mr-2 min-h-0 overflow-y-auto overscroll-contain pr-2 pb-6 [scrollbar-width:thin]"
|
|
335
|
+
>
|
|
336
|
+
{sidePanel}
|
|
337
|
+
</aside>
|
|
338
|
+
) : (
|
|
339
|
+
<Sheet open={detailsOpen} onOpenChange={setDetailsOpen}>
|
|
340
|
+
<SheetContent side="right" className="w-[88vw] gap-0 overflow-y-auto sm:max-w-sm">
|
|
341
|
+
<SheetHeader className="pb-2">
|
|
342
|
+
<SheetTitle>{project.name}</SheetTitle>
|
|
343
|
+
<SheetDescription>Instructions, MCP servers, and skills.</SheetDescription>
|
|
344
|
+
</SheetHeader>
|
|
345
|
+
<div className="px-4 pt-2 pb-6">{sidePanel}</div>
|
|
346
|
+
</SheetContent>
|
|
347
|
+
</Sheet>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
{/* Mobile/tablet: aksi utama melayang di kanan-bawah (jangkauan jempol,
|
|
351
|
+
selalu terlihat saat scroll). Label teks tetap ada agar jelas —
|
|
352
|
+
bukan ikon "+" tanpa arti. Safe-area iOS dihormati. */}
|
|
353
|
+
{!wide && (
|
|
354
|
+
<Button
|
|
355
|
+
type="button"
|
|
356
|
+
onClick={openForm}
|
|
357
|
+
className="fixed right-4 bottom-[max(1rem,env(safe-area-inset-bottom))] z-30 h-12 rounded-full px-5 text-[15px] shadow-lg shadow-primary/25 sm:right-6 sm:bottom-6"
|
|
358
|
+
>
|
|
359
|
+
<PlusIcon data-icon="inline-start" className="size-5" />
|
|
360
|
+
New session
|
|
361
|
+
</Button>
|
|
362
|
+
)}
|
|
363
|
+
|
|
364
|
+
{/* Form pembuatan Session — dialog agar daftar tidak terdorong di HP */}
|
|
365
|
+
<NewSessionDialog
|
|
366
|
+
open={formOpen}
|
|
367
|
+
onOpenChange={setFormOpen}
|
|
368
|
+
projectId={project.id}
|
|
369
|
+
onCreated={() => {
|
|
370
|
+
void list.refresh();
|
|
371
|
+
notifyDataChanged();
|
|
372
|
+
}}
|
|
373
|
+
/>
|
|
374
|
+
|
|
375
|
+
{/* Konfirmasi hapus Session permanen */}
|
|
376
|
+
<ConfirmSessionDeleteDialog
|
|
377
|
+
open={list.pendingDelete !== null}
|
|
378
|
+
onOpenChange={(open) => {
|
|
379
|
+
if (!open) list.cancelDelete();
|
|
380
|
+
}}
|
|
381
|
+
deleting={list.deleting !== null}
|
|
382
|
+
onConfirm={() => void list.confirmRemove()}
|
|
383
|
+
/>
|
|
384
|
+
|
|
385
|
+
{/* Konfirmasi hapus Project (beserta seluruh Session-nya) */}
|
|
386
|
+
<ConfirmDeleteProjectDialog
|
|
387
|
+
open={list.projectDeleteOpen}
|
|
388
|
+
onOpenChange={(open) => (open ? list.openProjectDelete() : list.closeProjectDelete())}
|
|
389
|
+
projectName={project.name}
|
|
390
|
+
sessionTotal={list.summary.total}
|
|
391
|
+
deleting={list.deletingProject}
|
|
392
|
+
onConfirm={() => void list.confirmRemoveProject()}
|
|
393
|
+
/>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|