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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer sidebar: profil pemilik (avatar + nickname) dengan tombol Settings
|
|
3
|
+
* dan Lock. Bila kunci belum diatur, tombol Lock diganti peringatan
|
|
4
|
+
* "Not protected" yang membuka `/settings/security`.
|
|
5
|
+
*/
|
|
6
|
+
import { LockIcon, SettingsIcon, ShieldAlertIcon } from "lucide-react";
|
|
7
|
+
import { ProfileAvatar } from "@/components/auth/ProfileAvatar";
|
|
8
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
9
|
+
import { lockNow, useAuth } from "@/lib/auth";
|
|
10
|
+
import { settingsPath } from "@/lib/routes";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
|
|
13
|
+
export interface SidebarAccountProps {
|
|
14
|
+
/** Halaman Settings sedang terbuka. */
|
|
15
|
+
active: boolean;
|
|
16
|
+
onNavigate: (path: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SidebarAccount({ active, onNavigate }: SidebarAccountProps) {
|
|
20
|
+
const auth = useAuth();
|
|
21
|
+
const status = auth.phase === "ready" ? auth.status : null;
|
|
22
|
+
const nickname = status?.profile.nickname ?? null;
|
|
23
|
+
const avatarUrl = status?.profile.avatarUrl ?? null;
|
|
24
|
+
const avatarPreset = status?.profile.avatarPreset ?? null;
|
|
25
|
+
const protectedApp = status?.protected ?? false;
|
|
26
|
+
|
|
27
|
+
const iconBtn =
|
|
28
|
+
"flex size-8 shrink-0 items-center justify-center rounded-md text-sidebar-foreground/70 transition-colors outline-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring";
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex flex-col gap-1">
|
|
32
|
+
{status && !protectedApp && (
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={() => onNavigate(settingsPath("security"))}
|
|
36
|
+
className="flex items-center gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-2 text-left text-xs text-amber-800 transition-colors hover:bg-amber-500/15 dark:text-amber-300"
|
|
37
|
+
>
|
|
38
|
+
<ShieldAlertIcon className="size-4 shrink-0" aria-hidden />
|
|
39
|
+
<span className="min-w-0">
|
|
40
|
+
<span className="block font-medium">Not protected</span>
|
|
41
|
+
<span className="block text-amber-800/80 dark:text-amber-300/80">
|
|
42
|
+
Set a lock before exposing this app.
|
|
43
|
+
</span>
|
|
44
|
+
</span>
|
|
45
|
+
</button>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
49
|
+
<a
|
|
50
|
+
href={settingsPath()}
|
|
51
|
+
onClick={(e) => {
|
|
52
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
onNavigate(settingsPath());
|
|
55
|
+
}}
|
|
56
|
+
aria-current={active ? "page" : undefined}
|
|
57
|
+
className={cn(
|
|
58
|
+
"flex min-w-0 flex-1 items-center gap-2.5 rounded-md p-1.5 text-left transition-colors outline-none hover:bg-sidebar-accent focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
|
59
|
+
active && "bg-sidebar-accent",
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<ProfileAvatar
|
|
63
|
+
nickname={nickname}
|
|
64
|
+
avatarUrl={avatarUrl}
|
|
65
|
+
avatarPreset={avatarPreset}
|
|
66
|
+
className="size-8"
|
|
67
|
+
textClassName="text-xs"
|
|
68
|
+
/>
|
|
69
|
+
<span className="flex min-w-0 flex-col leading-tight">
|
|
70
|
+
<span className="truncate text-sm font-medium">{nickname ?? "Set up profile"}</span>
|
|
71
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
72
|
+
{protectedApp
|
|
73
|
+
? `Locked with ${status?.lockKind === "pin" ? "PIN" : "password"}`
|
|
74
|
+
: "Settings"}
|
|
75
|
+
</span>
|
|
76
|
+
</span>
|
|
77
|
+
</a>
|
|
78
|
+
|
|
79
|
+
<Tooltip>
|
|
80
|
+
<TooltipTrigger asChild>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => onNavigate(settingsPath())}
|
|
84
|
+
aria-label="Settings"
|
|
85
|
+
className={cn(iconBtn, active && "text-sidebar-accent-foreground")}
|
|
86
|
+
>
|
|
87
|
+
<SettingsIcon className="size-4" />
|
|
88
|
+
</button>
|
|
89
|
+
</TooltipTrigger>
|
|
90
|
+
<TooltipContent side="top">Settings</TooltipContent>
|
|
91
|
+
</Tooltip>
|
|
92
|
+
|
|
93
|
+
{protectedApp && (
|
|
94
|
+
<Tooltip>
|
|
95
|
+
<TooltipTrigger asChild>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => void lockNow()}
|
|
99
|
+
aria-label="Lock now"
|
|
100
|
+
className={iconBtn}
|
|
101
|
+
>
|
|
102
|
+
<LockIcon className="size-4" />
|
|
103
|
+
</button>
|
|
104
|
+
</TooltipTrigger>
|
|
105
|
+
<TooltipContent side="top">Lock now</TooltipContent>
|
|
106
|
+
</Tooltip>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu "..." per baris Session di sidebar — KHUSUS desktop.
|
|
3
|
+
*
|
|
4
|
+
* - Titik tiga vertikal di celah kosong kanan baris (di luar area hover
|
|
5
|
+
* baris). Muncul saat baris di-hover / difokus keyboard (`group-hover`),
|
|
6
|
+
* tetap terlihat selama menu terbuka.
|
|
7
|
+
* - Aksi sesuai status: running -> Stop & Restart; stopped/crashed -> Start.
|
|
8
|
+
* Delete selalu ada (dengan dialog konfirmasi).
|
|
9
|
+
* - Setelah aksi, sidebar & daftar lain terbarui otomatis lewat broadcast
|
|
10
|
+
* server (`session_status`, `session_deleted`, `data_changed`) — di sini
|
|
11
|
+
* tidak perlu memuat ulang manual.
|
|
12
|
+
*/
|
|
13
|
+
import { MoreVerticalIcon, PlayIcon, RotateCcwIcon, SquareIcon, Trash2Icon } from "lucide-react";
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { toast } from "sonner";
|
|
16
|
+
import { ConfirmSessionDeleteDialog } from "@/components/sessions/ConfirmSessionDeleteDialog";
|
|
17
|
+
import {
|
|
18
|
+
DropdownMenu,
|
|
19
|
+
DropdownMenuContent,
|
|
20
|
+
DropdownMenuItem,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from "@/components/ui/dropdown-menu";
|
|
24
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
25
|
+
import { ApiError, apiFetch } from "@/lib/api";
|
|
26
|
+
import { notifyDataChanged } from "@/lib/data-events";
|
|
27
|
+
import { cn } from "@/lib/utils";
|
|
28
|
+
import type { Session } from "@/types";
|
|
29
|
+
|
|
30
|
+
type Busy = "stop" | "start" | "restart" | null;
|
|
31
|
+
|
|
32
|
+
function errorText(e: unknown, fallback: string): string {
|
|
33
|
+
return e instanceof ApiError ? e.message : fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SidebarSessionMenuProps {
|
|
37
|
+
session: Session;
|
|
38
|
+
/** Judul tampilan (untuk label aksesibel). */
|
|
39
|
+
title: string;
|
|
40
|
+
/** Dipanggil setelah Session berhasil dihapus. */
|
|
41
|
+
onDeleted?: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function SidebarSessionMenu({ session, title, onDeleted }: SidebarSessionMenuProps) {
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
const [busy, setBusy] = useState<Busy>(null);
|
|
47
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
48
|
+
const [deleting, setDeleting] = useState(false);
|
|
49
|
+
const running = session.status === "running";
|
|
50
|
+
|
|
51
|
+
const stop = () => apiFetch(`/api/sessions/${session.id}/stop`, { method: "POST" });
|
|
52
|
+
const start = () => apiFetch(`/api/sessions/${session.id}`, { method: "POST" });
|
|
53
|
+
|
|
54
|
+
const run = async (kind: Exclude<Busy, null>) => {
|
|
55
|
+
setBusy(kind);
|
|
56
|
+
try {
|
|
57
|
+
if (kind === "stop") {
|
|
58
|
+
await stop();
|
|
59
|
+
toast.success("Session stopped.");
|
|
60
|
+
} else if (kind === "start") {
|
|
61
|
+
await start();
|
|
62
|
+
toast.success("Session started.");
|
|
63
|
+
} else {
|
|
64
|
+
await stop();
|
|
65
|
+
await start();
|
|
66
|
+
toast.success("Session restarted.");
|
|
67
|
+
}
|
|
68
|
+
notifyDataChanged();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
toast.error(
|
|
71
|
+
errorText(
|
|
72
|
+
e,
|
|
73
|
+
kind === "stop"
|
|
74
|
+
? "Couldn't stop the session."
|
|
75
|
+
: kind === "start"
|
|
76
|
+
? "Couldn't start the session."
|
|
77
|
+
: "Couldn't restart the session.",
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
} finally {
|
|
81
|
+
setBusy(null);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const remove = async () => {
|
|
86
|
+
setDeleting(true);
|
|
87
|
+
try {
|
|
88
|
+
await apiFetch(`/api/sessions/${session.id}`, { method: "DELETE" });
|
|
89
|
+
setConfirmDelete(false);
|
|
90
|
+
toast.success("Session deleted.");
|
|
91
|
+
notifyDataChanged();
|
|
92
|
+
onDeleted?.();
|
|
93
|
+
} catch (e) {
|
|
94
|
+
toast.error(errorText(e, "Couldn't delete the session."));
|
|
95
|
+
} finally {
|
|
96
|
+
setDeleting(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
103
|
+
<DropdownMenuTrigger asChild>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
aria-label={`Session actions: ${title}`}
|
|
107
|
+
disabled={busy !== null}
|
|
108
|
+
className={cn(
|
|
109
|
+
// Diselipkan di celah kosong KANAN baris (margin kanan
|
|
110
|
+
// `SidebarMenuSub` = 24px), bukan menimpa judul: lebar 24px
|
|
111
|
+
// menempel tepat di tepi baris agar hover tidak "putus" saat
|
|
112
|
+
// kursor bergeser dari baris ke tombol.
|
|
113
|
+
"absolute top-1/2 -right-6 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-sidebar-foreground/60 outline-hidden transition-opacity",
|
|
114
|
+
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
|
115
|
+
// Tersembunyi sampai baris di-hover / difokus; tetap terlihat
|
|
116
|
+
// selama menu terbuka atau aksi berjalan.
|
|
117
|
+
"opacity-0 group-focus-within/menu-sub-item:opacity-100 group-hover/menu-sub-item:opacity-100 data-[state=open]:opacity-100",
|
|
118
|
+
busy !== null && "opacity-100",
|
|
119
|
+
"[&>svg]:size-4",
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{busy !== null ? <Spinner className="size-3.5" /> : <MoreVerticalIcon />}
|
|
123
|
+
</button>
|
|
124
|
+
</DropdownMenuTrigger>
|
|
125
|
+
<DropdownMenuContent side="right" align="start" className="min-w-40">
|
|
126
|
+
{running ? (
|
|
127
|
+
<>
|
|
128
|
+
<DropdownMenuItem onSelect={() => void run("stop")}>
|
|
129
|
+
<SquareIcon />
|
|
130
|
+
Stop
|
|
131
|
+
</DropdownMenuItem>
|
|
132
|
+
<DropdownMenuItem onSelect={() => void run("restart")}>
|
|
133
|
+
<RotateCcwIcon />
|
|
134
|
+
Restart
|
|
135
|
+
</DropdownMenuItem>
|
|
136
|
+
</>
|
|
137
|
+
) : (
|
|
138
|
+
<DropdownMenuItem onSelect={() => void run("start")}>
|
|
139
|
+
<PlayIcon />
|
|
140
|
+
{session.status === "crashed" ? "Restart" : "Start"}
|
|
141
|
+
</DropdownMenuItem>
|
|
142
|
+
)}
|
|
143
|
+
<DropdownMenuSeparator />
|
|
144
|
+
<DropdownMenuItem
|
|
145
|
+
variant="destructive"
|
|
146
|
+
// Tunda buka dialog sampai menu selesai menutup (fokus aman).
|
|
147
|
+
onSelect={() => setConfirmDelete(true)}
|
|
148
|
+
>
|
|
149
|
+
<Trash2Icon />
|
|
150
|
+
Delete
|
|
151
|
+
</DropdownMenuItem>
|
|
152
|
+
</DropdownMenuContent>
|
|
153
|
+
</DropdownMenu>
|
|
154
|
+
|
|
155
|
+
<ConfirmSessionDeleteDialog
|
|
156
|
+
open={confirmDelete}
|
|
157
|
+
onOpenChange={setConfirmDelete}
|
|
158
|
+
deleting={deleting}
|
|
159
|
+
onConfirm={() => void remove()}
|
|
160
|
+
/>
|
|
161
|
+
</>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pemilih tema (Requirement 8.2) di footer sidebar: label "Theme" di kiri,
|
|
3
|
+
* tiga tombol ikon di kanan — Light, Dark, System (ikuti perangkat) — dalam
|
|
4
|
+
* satu segmented control ringkas. Pilihan disimpan `useTheme` di
|
|
5
|
+
* `localStorage["kcg-theme"]` dan diterapkan ke `<html>` (Requirement 8.6).
|
|
6
|
+
*
|
|
7
|
+
* Aksesibilitas: grup radio native (`<input type="radio">` tersembunyi di
|
|
8
|
+
* dalam label), jadi Tab masuk ke pilihan aktif dan panah berpindah opsi.
|
|
9
|
+
* Tombol hanya ikon, jadi nama tiap opsi diberikan lewat teks `sr-only` dan
|
|
10
|
+
* tooltip (`title`).
|
|
11
|
+
*/
|
|
12
|
+
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
|
|
13
|
+
import { useId } from "react";
|
|
14
|
+
import { type ThemePreference, useTheme } from "@/hooks/useTheme";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
|
|
17
|
+
const OPTIONS: { value: ThemePreference; label: string; Icon: typeof SunIcon }[] = [
|
|
18
|
+
{ value: "light", label: "Light", Icon: SunIcon },
|
|
19
|
+
{ value: "dark", label: "Dark", Icon: MoonIcon },
|
|
20
|
+
{ value: "system", label: "System", Icon: MonitorIcon },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function ThemeToggle({ className }: { className?: string }) {
|
|
24
|
+
const { preference, setPreference } = useTheme();
|
|
25
|
+
const name = useId();
|
|
26
|
+
const labelId = useId();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={cn("flex items-center justify-between gap-3 px-2", className)}>
|
|
30
|
+
<span id={labelId} className="text-sm text-sidebar-foreground/80">
|
|
31
|
+
Theme
|
|
32
|
+
</span>
|
|
33
|
+
<fieldset
|
|
34
|
+
aria-labelledby={labelId}
|
|
35
|
+
className="flex shrink-0 items-center gap-0.5 rounded-lg border border-sidebar-border bg-sidebar-accent/60 p-0.5"
|
|
36
|
+
>
|
|
37
|
+
{OPTIONS.map(({ value, label, Icon }) => {
|
|
38
|
+
const active = preference === value;
|
|
39
|
+
return (
|
|
40
|
+
<label
|
|
41
|
+
key={value}
|
|
42
|
+
title={value === "system" ? "System (match device)" : label}
|
|
43
|
+
className={cn(
|
|
44
|
+
"flex size-7 cursor-pointer items-center justify-center rounded-md transition-colors select-none",
|
|
45
|
+
"has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-sidebar-ring",
|
|
46
|
+
active
|
|
47
|
+
? "bg-sidebar text-sidebar-foreground shadow-sm"
|
|
48
|
+
: "text-sidebar-foreground/55 hover:text-sidebar-foreground",
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<input
|
|
52
|
+
type="radio"
|
|
53
|
+
name={name}
|
|
54
|
+
value={value}
|
|
55
|
+
checked={active}
|
|
56
|
+
onChange={() => setPreference(value)}
|
|
57
|
+
className="sr-only"
|
|
58
|
+
/>
|
|
59
|
+
<Icon className="size-4 shrink-0" aria-hidden />
|
|
60
|
+
<span className="sr-only">{label}</span>
|
|
61
|
+
</label>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</fieldset>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folder_Browser (Requirement 10.2, 10.3).
|
|
3
|
+
*
|
|
4
|
+
* Menjelajah direktori di dalam Sandbox_Root via `GET /api/fs?path=...`:
|
|
5
|
+
* breadcrumb path saat ini dan daftar sub-direktori. Fully controlled —
|
|
6
|
+
* direktori yang sedang dijelajahi (`path`) adalah direktori yang otomatis
|
|
7
|
+
* aktif/terpilih (Requirement 10.4); tidak ada tombol "Pilih" terpisah.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ChevronRightIcon, FolderIcon, Undo2Icon } from "lucide-react";
|
|
11
|
+
import { useCallback, useEffect, useState } from "react";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
14
|
+
import { ApiError, apiFetch } from "@/lib/api";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
|
|
17
|
+
export interface FolderBrowserProps {
|
|
18
|
+
/** Path relatif yang sedang aktif/dijelajahi ("" = Sandbox_Root). */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Dipanggil setiap kali pengguna berpindah direktori. */
|
|
21
|
+
onNavigate: (path: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function joinDir(base: string, name: string): string {
|
|
25
|
+
return base === "" ? name : `${base}/${name}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function FolderBrowser({ path, onNavigate }: FolderBrowserProps) {
|
|
29
|
+
const [entries, setEntries] = useState<string[]>([]);
|
|
30
|
+
const [loading, setLoading] = useState(false);
|
|
31
|
+
const [error, setError] = useState<string | null>(null);
|
|
32
|
+
|
|
33
|
+
const load = useCallback(async (p: string) => {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
setError(null);
|
|
36
|
+
try {
|
|
37
|
+
const res = await apiFetch(`/api/fs?path=${encodeURIComponent(p)}`);
|
|
38
|
+
const body = (await res.json()) as { entries: string[] };
|
|
39
|
+
setEntries(body.entries);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
setEntries([]);
|
|
42
|
+
setError(e instanceof ApiError ? e.message : "Failed to load directory");
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
load(path);
|
|
50
|
+
}, [path, load]);
|
|
51
|
+
|
|
52
|
+
const segments = path === "" ? [] : path.split("/");
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex h-full min-h-0 flex-col gap-2">
|
|
56
|
+
{/* Breadcrumb */}
|
|
57
|
+
<div className="flex shrink-0 flex-wrap items-center gap-1 text-sm">
|
|
58
|
+
<Button
|
|
59
|
+
type="button"
|
|
60
|
+
variant="ghost"
|
|
61
|
+
size="sm"
|
|
62
|
+
className={cn("h-7 px-2 font-medium", path === "" && "text-foreground")}
|
|
63
|
+
onClick={() => onNavigate("")}
|
|
64
|
+
>
|
|
65
|
+
Sandbox
|
|
66
|
+
</Button>
|
|
67
|
+
{segments.map((seg, i) => {
|
|
68
|
+
const target = segments.slice(0, i + 1).join("/");
|
|
69
|
+
return (
|
|
70
|
+
<span key={target} className="flex items-center gap-1">
|
|
71
|
+
<ChevronRightIcon
|
|
72
|
+
className="size-3.5 text-muted-foreground"
|
|
73
|
+
data-icon="inline-start"
|
|
74
|
+
/>
|
|
75
|
+
<Button
|
|
76
|
+
type="button"
|
|
77
|
+
variant="ghost"
|
|
78
|
+
size="sm"
|
|
79
|
+
className={cn(
|
|
80
|
+
"h-7 px-2 font-medium",
|
|
81
|
+
i === segments.length - 1 && "text-foreground",
|
|
82
|
+
)}
|
|
83
|
+
onClick={() => onNavigate(target)}
|
|
84
|
+
>
|
|
85
|
+
{seg}
|
|
86
|
+
</Button>
|
|
87
|
+
</span>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Daftar sub-direktori */}
|
|
93
|
+
<div className="min-h-0 flex-1 overflow-y-auto rounded-lg border bg-card p-2">
|
|
94
|
+
{loading ? (
|
|
95
|
+
<div className="flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground">
|
|
96
|
+
<Spinner className="size-4" />
|
|
97
|
+
Loading directories…
|
|
98
|
+
</div>
|
|
99
|
+
) : error ? (
|
|
100
|
+
<p className="px-2 py-4 text-sm text-destructive">{error}</p>
|
|
101
|
+
) : entries.length === 0 ? (
|
|
102
|
+
<p className="px-2 py-4 text-sm text-muted-foreground">No subdirectories.</p>
|
|
103
|
+
) : (
|
|
104
|
+
<div className="flex flex-col gap-1">
|
|
105
|
+
{path !== "" && (
|
|
106
|
+
<Button
|
|
107
|
+
type="button"
|
|
108
|
+
variant="ghost"
|
|
109
|
+
size="sm"
|
|
110
|
+
className="h-8 justify-start gap-2 px-2 text-muted-foreground"
|
|
111
|
+
onClick={() => onNavigate(segments.slice(0, -1).join("/"))}
|
|
112
|
+
>
|
|
113
|
+
<Undo2Icon data-icon="inline-start" />
|
|
114
|
+
Back
|
|
115
|
+
</Button>
|
|
116
|
+
)}
|
|
117
|
+
{entries.map((name) => (
|
|
118
|
+
<Button
|
|
119
|
+
key={name}
|
|
120
|
+
type="button"
|
|
121
|
+
variant="ghost"
|
|
122
|
+
size="sm"
|
|
123
|
+
className="h-8 justify-start gap-2 px-2"
|
|
124
|
+
onClick={() => onNavigate(joinDir(path, name))}
|
|
125
|
+
>
|
|
126
|
+
<FolderIcon data-icon="inline-start" />
|
|
127
|
+
<span className="truncate">{name}</span>
|
|
128
|
+
</Button>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Path aktif saat ini — selalu nempel di bawah, tidak ikut naik saat
|
|
135
|
+
daftar sub-direktori pendek. */}
|
|
136
|
+
<div className="flex min-w-0 shrink-0 items-center gap-1.5 text-sm">
|
|
137
|
+
<span className="shrink-0 text-muted-foreground">Current directory:</span>
|
|
138
|
+
<span className="min-w-0 flex-1 truncate font-mono">{path === "" ? "/" : path}</span>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|