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,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings -> About: logo + versi KCG Code, lalu kredit tim.
|
|
3
|
+
*
|
|
4
|
+
* Versi dari `GET /api/meta` — `"dev"` saat dijalankan lewat `bun dev`.
|
|
5
|
+
* Tiap anggota tim adalah baris tautan ke profil GitHub-nya (tab baru).
|
|
6
|
+
*/
|
|
7
|
+
import type * as React from "react";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
10
|
+
import { apiFetch } from "@/lib/api";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
import type { AppInfo } from "@/server/services/app-info";
|
|
13
|
+
import logo from "../../logo.svg";
|
|
14
|
+
|
|
15
|
+
export interface TeamMember {
|
|
16
|
+
/** Username GitHub (tanpa `@`). */
|
|
17
|
+
username: string;
|
|
18
|
+
role: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const TEAM: readonly TeamMember[] = [
|
|
22
|
+
{ username: "irsyadulibad", role: "Lead Developer" },
|
|
23
|
+
{ username: "sahrullahh", role: "Product Advisor" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function githubUrl(username: string): string {
|
|
27
|
+
return `https://github.com/${encodeURIComponent(username)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Teks versi untuk ditampilkan: `"dev"` apa adanya, semver diberi `v`. */
|
|
31
|
+
export function formatVersion(version: string): string {
|
|
32
|
+
return version === "dev" ? "dev" : `v${version}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function AboutGroup() {
|
|
36
|
+
const [version, setVersion] = useState<string | null>(null);
|
|
37
|
+
const [failed, setFailed] = useState(false);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let cancelled = false;
|
|
41
|
+
void (async () => {
|
|
42
|
+
try {
|
|
43
|
+
const res = await apiFetch("/api/meta");
|
|
44
|
+
const info = (await res.json()) as AppInfo;
|
|
45
|
+
if (!cancelled) setVersion(info.version);
|
|
46
|
+
} catch {
|
|
47
|
+
if (!cancelled) setFailed(true);
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return <AboutView version={version} failed={failed} />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Tampilan murni (tanpa fetch) — dipakai komponen di atas & diuji SSR. */
|
|
59
|
+
export function AboutView({ version, failed }: { version: string | null; failed: boolean }) {
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
{/* Hero: kartu netral dengan SATU cahaya lembut dari atas (warna logo,
|
|
63
|
+
opasitas rendah) + garis highlight tipis di tepi atas. Sengaja
|
|
64
|
+
tanpa pola titik, blob blur, teks gradien, atau animasi — aksen
|
|
65
|
+
cukup satu, sisanya tipografi & ruang kosong. */}
|
|
66
|
+
<section
|
|
67
|
+
aria-labelledby="about-title"
|
|
68
|
+
className="relative isolate overflow-hidden rounded-3xl border bg-card px-6 pt-12 pb-9 text-center shadow-xs"
|
|
69
|
+
>
|
|
70
|
+
<div
|
|
71
|
+
aria-hidden
|
|
72
|
+
className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(70%_55%_at_50%_0%,rgba(109,92,251,0.09),transparent_70%)] dark:bg-[radial-gradient(70%_55%_at_50%_0%,rgba(109,92,251,0.16),transparent_70%)]"
|
|
73
|
+
/>
|
|
74
|
+
<div
|
|
75
|
+
aria-hidden
|
|
76
|
+
className="pointer-events-none absolute inset-x-12 top-0 h-px bg-gradient-to-r from-transparent via-[#6D5CFB]/45 to-transparent"
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<div className="flex flex-col items-center">
|
|
80
|
+
<span className="flex size-24 items-center justify-center rounded-[1.75rem] border bg-background p-5 shadow-sm sm:size-28">
|
|
81
|
+
<img src={logo} alt="KCG Code logo" className="size-full" />
|
|
82
|
+
</span>
|
|
83
|
+
|
|
84
|
+
<h2 id="about-title" className="mt-6 text-2xl font-semibold tracking-tight sm:text-3xl">
|
|
85
|
+
KCG Code
|
|
86
|
+
</h2>
|
|
87
|
+
<p className="mt-1.5 max-w-xs text-[14px] leading-snug text-pretty text-muted-foreground">
|
|
88
|
+
Your AI coding agent, in your pocket.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<div className="mt-5">
|
|
92
|
+
{version !== null ? (
|
|
93
|
+
<span
|
|
94
|
+
className="inline-flex items-center gap-1.5 rounded-full border bg-background px-3 py-1 font-mono text-[12.5px] text-muted-foreground"
|
|
95
|
+
title={version === "dev" ? "Running from source (bun dev)" : undefined}
|
|
96
|
+
>
|
|
97
|
+
<span
|
|
98
|
+
aria-hidden
|
|
99
|
+
className={cn(
|
|
100
|
+
"size-1.5 rounded-full",
|
|
101
|
+
version === "dev" ? "bg-amber-500" : "bg-emerald-500",
|
|
102
|
+
)}
|
|
103
|
+
/>
|
|
104
|
+
<span className="text-foreground/80">{formatVersion(version)}</span>
|
|
105
|
+
</span>
|
|
106
|
+
) : failed ? (
|
|
107
|
+
<span className="text-[13px] text-muted-foreground">Version unavailable</span>
|
|
108
|
+
) : (
|
|
109
|
+
<Skeleton className="h-7 w-20 rounded-full" aria-label="Loading version" />
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</section>
|
|
114
|
+
|
|
115
|
+
{/* Credits: kartu profil per anggota (bukan baris preferensi). */}
|
|
116
|
+
<section aria-labelledby="about-credits" className="flex flex-col gap-3">
|
|
117
|
+
<div className="flex flex-col gap-0.5 px-1">
|
|
118
|
+
<h2 id="about-credits" className="text-[15px] font-semibold tracking-tight">
|
|
119
|
+
Credits
|
|
120
|
+
</h2>
|
|
121
|
+
<p className="text-[13px] text-muted-foreground">The people building KCG Code.</p>
|
|
122
|
+
</div>
|
|
123
|
+
<ul className="grid gap-3 sm:grid-cols-2">
|
|
124
|
+
{TEAM.map((m) => (
|
|
125
|
+
<li key={m.username}>
|
|
126
|
+
<a
|
|
127
|
+
href={githubUrl(m.username)}
|
|
128
|
+
target="_blank"
|
|
129
|
+
rel="noopener noreferrer"
|
|
130
|
+
className="group flex h-full items-center gap-3.5 rounded-2xl border bg-card p-4 shadow-xs transition-colors outline-none hover:border-foreground/20 hover:bg-muted/40 focus-visible:ring-[3px] focus-visible:ring-ring/50 active:bg-muted/70"
|
|
131
|
+
>
|
|
132
|
+
<img
|
|
133
|
+
src={`${githubUrl(m.username)}.png?size=112`}
|
|
134
|
+
alt=""
|
|
135
|
+
width={44}
|
|
136
|
+
height={44}
|
|
137
|
+
loading="lazy"
|
|
138
|
+
className="size-11 shrink-0 rounded-full bg-muted ring-1 ring-border"
|
|
139
|
+
/>
|
|
140
|
+
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
141
|
+
<span className="truncate text-[15px] leading-5 font-medium">@{m.username}</span>
|
|
142
|
+
<span className="text-[13px] leading-snug text-muted-foreground">{m.role}</span>
|
|
143
|
+
</span>
|
|
144
|
+
<GithubIcon className="size-4 shrink-0 text-muted-foreground transition-colors group-hover:text-foreground" />
|
|
145
|
+
<span className="sr-only">(opens GitHub in a new tab)</span>
|
|
146
|
+
</a>
|
|
147
|
+
</li>
|
|
148
|
+
))}
|
|
149
|
+
</ul>
|
|
150
|
+
</section>
|
|
151
|
+
|
|
152
|
+
<p className="pb-2 text-center text-[12px] text-muted-foreground">
|
|
153
|
+
Built with Bun & OpenCode.
|
|
154
|
+
</p>
|
|
155
|
+
</>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Mark GitHub (lucide versi ini tidak lagi menyertakan ikon brand).
|
|
161
|
+
* Path resmi dari GitHub Octicons `mark-github`.
|
|
162
|
+
*/
|
|
163
|
+
function GithubIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
164
|
+
return (
|
|
165
|
+
// Selalu dekoratif: label tautan sudah ada di teks baris.
|
|
166
|
+
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" {...props}>
|
|
167
|
+
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z" />
|
|
168
|
+
</svg>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings -> Profile: akun KCG Code yang tersambung ke mesin ini (akun yang
|
|
3
|
+
* dipakai Remote access). Data dari `GET /api/tunnel` — hanya email,
|
|
4
|
+
* username, dan alamat; token & secret tidak pernah dikirim ke browser.
|
|
5
|
+
*/
|
|
6
|
+
import { GlobeIcon, UserRoundIcon } from "lucide-react";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { ActionRow, PrefsGroup, RowBadge } from "@/components/settings/prefs";
|
|
9
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
10
|
+
import { useRouter } from "@/hooks/useRouter";
|
|
11
|
+
import { apiFetch } from "@/lib/api";
|
|
12
|
+
import { settingsPath } from "@/lib/routes";
|
|
13
|
+
import type { TunnelStatus } from "@/server/services/tunnel";
|
|
14
|
+
|
|
15
|
+
type AccountState =
|
|
16
|
+
| { phase: "loading" }
|
|
17
|
+
| { phase: "error" }
|
|
18
|
+
| { phase: "ready"; configured: boolean; account: TunnelStatus["account"] };
|
|
19
|
+
|
|
20
|
+
export function AccountGroup() {
|
|
21
|
+
const { navigate } = useRouter();
|
|
22
|
+
const [state, setState] = useState<AccountState>({ phase: "loading" });
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
void (async () => {
|
|
27
|
+
try {
|
|
28
|
+
const res = await apiFetch("/api/tunnel");
|
|
29
|
+
const { tunnel } = (await res.json()) as { tunnel: TunnelStatus };
|
|
30
|
+
if (!cancelled) {
|
|
31
|
+
setState({ phase: "ready", configured: tunnel.configured, account: tunnel.account });
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
if (!cancelled) setState({ phase: "error" });
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const title = "Account";
|
|
43
|
+
|
|
44
|
+
if (state.phase === "loading") {
|
|
45
|
+
return (
|
|
46
|
+
<PrefsGroup title={title}>
|
|
47
|
+
<li className="flex min-h-16 items-center justify-center" role="status">
|
|
48
|
+
<Spinner className="size-5 text-muted-foreground" />
|
|
49
|
+
<span className="sr-only">Loading account…</span>
|
|
50
|
+
</li>
|
|
51
|
+
</PrefsGroup>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (state.phase === "error") {
|
|
56
|
+
return (
|
|
57
|
+
<PrefsGroup title={title}>
|
|
58
|
+
<ActionRow title="Couldn't load account" subtitle="Reload the page to try again." />
|
|
59
|
+
</PrefsGroup>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { account, configured } = state;
|
|
64
|
+
const goRemote = () => navigate(settingsPath("remote"));
|
|
65
|
+
|
|
66
|
+
if (!account) {
|
|
67
|
+
return (
|
|
68
|
+
<PrefsGroup title={title}>
|
|
69
|
+
<ActionRow
|
|
70
|
+
prefix={<AccountIcon />}
|
|
71
|
+
title="No account linked"
|
|
72
|
+
subtitle={
|
|
73
|
+
configured
|
|
74
|
+
? "Sign in with Google in Remote access to use KCG Code from anywhere"
|
|
75
|
+
: "Remote access isn't available in this version"
|
|
76
|
+
}
|
|
77
|
+
onActivate={configured ? goRemote : undefined}
|
|
78
|
+
/>
|
|
79
|
+
</PrefsGroup>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<PrefsGroup title={title}>
|
|
85
|
+
{/* Email jadi baris utama (yang dikenali pengguna); username sekunder. */}
|
|
86
|
+
<li className="flex items-center gap-3 px-4 py-3">
|
|
87
|
+
<AccountIcon />
|
|
88
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
89
|
+
<span className="truncate text-[15px] leading-5" title={account.email}>
|
|
90
|
+
{account.email}
|
|
91
|
+
</span>
|
|
92
|
+
<span className="truncate text-[13px] text-muted-foreground">
|
|
93
|
+
Signed in with Google · @{account.username}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
<RowBadge tone="success">Linked</RowBadge>
|
|
97
|
+
</li>
|
|
98
|
+
<ActionRow
|
|
99
|
+
prefix={
|
|
100
|
+
<span className="flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
101
|
+
<GlobeIcon className="size-5" aria-hidden />
|
|
102
|
+
</span>
|
|
103
|
+
}
|
|
104
|
+
title="Remote access"
|
|
105
|
+
subtitle={
|
|
106
|
+
<span className="block truncate" title={account.url}>
|
|
107
|
+
{account.url.replace(/^https?:\/\//, "")}
|
|
108
|
+
</span>
|
|
109
|
+
}
|
|
110
|
+
onActivate={goRemote}
|
|
111
|
+
/>
|
|
112
|
+
</PrefsGroup>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function AccountIcon() {
|
|
117
|
+
return (
|
|
118
|
+
<span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
119
|
+
<UserRoundIcon className="size-5" aria-hidden />
|
|
120
|
+
</span>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings -> Security -> Signed-in devices (`/settings/security#devices`,
|
|
3
|
+
* URL lama `/settings/devices` dialihkan ke sini): sesi login per perangkat,
|
|
4
|
+
* bisa di-sign-out satu per satu atau sekaligus.
|
|
5
|
+
*/
|
|
6
|
+
import { LaptopIcon, SmartphoneIcon } from "lucide-react";
|
|
7
|
+
import { useCallback, useEffect, useState } from "react";
|
|
8
|
+
import { ActionRow, PrefsGroup, RowBadge } from "@/components/settings/prefs";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
11
|
+
import { ApiError, apiFetch } from "@/lib/api";
|
|
12
|
+
import type { DeviceInfo } from "@/server/services/auth";
|
|
13
|
+
|
|
14
|
+
function errorText(e: unknown, fallback: string): string {
|
|
15
|
+
return e instanceof ApiError ? e.message : fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Ringkasan user agent -> "Chrome on Android". */
|
|
19
|
+
export function describeDevice(ua: string | null): { label: string; mobile: boolean } {
|
|
20
|
+
if (!ua) return { label: "Unknown device", mobile: false };
|
|
21
|
+
const mobile = /Android|iPhone|iPad|Mobile/i.test(ua);
|
|
22
|
+
const browser = /Edg\//.test(ua)
|
|
23
|
+
? "Edge"
|
|
24
|
+
: /OPR\/|Opera/.test(ua)
|
|
25
|
+
? "Opera"
|
|
26
|
+
: /Firefox\//.test(ua)
|
|
27
|
+
? "Firefox"
|
|
28
|
+
: /Chrome\//.test(ua)
|
|
29
|
+
? "Chrome"
|
|
30
|
+
: /Safari\//.test(ua)
|
|
31
|
+
? "Safari"
|
|
32
|
+
: "Browser";
|
|
33
|
+
const os = /Android/.test(ua)
|
|
34
|
+
? "Android"
|
|
35
|
+
: /iPhone|iPad|iOS/.test(ua)
|
|
36
|
+
? "iOS"
|
|
37
|
+
: /Mac OS X|Macintosh/.test(ua)
|
|
38
|
+
? "macOS"
|
|
39
|
+
: /Windows/.test(ua)
|
|
40
|
+
? "Windows"
|
|
41
|
+
: /Linux/.test(ua)
|
|
42
|
+
? "Linux"
|
|
43
|
+
: "";
|
|
44
|
+
return { label: os ? `${browser} on ${os}` : browser, mobile };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function relativeTime(ts: number): string {
|
|
48
|
+
const s = Math.round((Date.now() - ts) / 1000);
|
|
49
|
+
if (s < 60) return "Active now";
|
|
50
|
+
const m = Math.round(s / 60);
|
|
51
|
+
if (m < 60) return `Active ${m} min ago`;
|
|
52
|
+
const h = Math.round(m / 60);
|
|
53
|
+
if (h < 24) return `Active ${h} h ago`;
|
|
54
|
+
return `Active ${Math.round(h / 24)} d ago`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function DevicesGroup({ onError }: { onError: (t: string) => void }) {
|
|
58
|
+
const [devices, setDevices] = useState<DeviceInfo[] | null>(null);
|
|
59
|
+
const [busy, setBusy] = useState(false);
|
|
60
|
+
|
|
61
|
+
const load = useCallback(async () => {
|
|
62
|
+
try {
|
|
63
|
+
const res = await apiFetch("/api/auth/devices");
|
|
64
|
+
setDevices(((await res.json()) as { devices: DeviceInfo[] }).devices);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
onError(errorText(err, "Couldn't load devices."));
|
|
67
|
+
}
|
|
68
|
+
}, [onError]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
void load();
|
|
72
|
+
}, [load]);
|
|
73
|
+
|
|
74
|
+
const revoke = async (path: string, init: RequestInit) => {
|
|
75
|
+
setBusy(true);
|
|
76
|
+
try {
|
|
77
|
+
await apiFetch(path, init);
|
|
78
|
+
await load();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
onError(errorText(err, "Couldn't sign out the device."));
|
|
81
|
+
} finally {
|
|
82
|
+
setBusy(false);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const others = devices?.filter((d) => !d.current) ?? [];
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<PrefsGroup
|
|
90
|
+
id="devices"
|
|
91
|
+
title="Signed-in devices"
|
|
92
|
+
description="Devices that have unlocked KCG Code."
|
|
93
|
+
suffix={
|
|
94
|
+
others.length > 0 ? (
|
|
95
|
+
<Button
|
|
96
|
+
type="button"
|
|
97
|
+
variant="ghost"
|
|
98
|
+
size="sm"
|
|
99
|
+
disabled={busy}
|
|
100
|
+
onClick={() => void revoke("/api/auth/devices/revoke-others", { method: "POST" })}
|
|
101
|
+
>
|
|
102
|
+
Sign out others
|
|
103
|
+
</Button>
|
|
104
|
+
) : undefined
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
{devices === null ? (
|
|
108
|
+
<li className="flex min-h-14 items-center justify-center" role="status">
|
|
109
|
+
<Spinner className="size-5 text-muted-foreground" />
|
|
110
|
+
</li>
|
|
111
|
+
) : (
|
|
112
|
+
devices.map((d) => {
|
|
113
|
+
const info = describeDevice(d.userAgent);
|
|
114
|
+
const Icon = info.mobile ? SmartphoneIcon : LaptopIcon;
|
|
115
|
+
return (
|
|
116
|
+
<ActionRow
|
|
117
|
+
key={d.id}
|
|
118
|
+
prefix={
|
|
119
|
+
<span className="flex size-9 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
120
|
+
<Icon className="size-[18px]" aria-hidden />
|
|
121
|
+
</span>
|
|
122
|
+
}
|
|
123
|
+
title={info.label}
|
|
124
|
+
subtitle={relativeTime(d.lastSeenAt)}
|
|
125
|
+
suffix={
|
|
126
|
+
d.current ? (
|
|
127
|
+
<RowBadge>This device</RowBadge>
|
|
128
|
+
) : (
|
|
129
|
+
<Button
|
|
130
|
+
type="button"
|
|
131
|
+
variant="outline"
|
|
132
|
+
size="sm"
|
|
133
|
+
disabled={busy}
|
|
134
|
+
onClick={() =>
|
|
135
|
+
void revoke(`/api/auth/devices/${encodeURIComponent(d.id)}`, {
|
|
136
|
+
method: "DELETE",
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
aria-label={`Sign out ${info.label}`}
|
|
140
|
+
>
|
|
141
|
+
Sign out
|
|
142
|
+
</Button>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
})
|
|
148
|
+
)}
|
|
149
|
+
</PrefsGroup>
|
|
150
|
+
);
|
|
151
|
+
}
|