kcg-code 0.1.0 → 0.2.1
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 +2 -0
- package/package.json +2 -1
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +170 -8
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +69 -6
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +174 -21
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +880 -159
- package/src/cli/index.ts +10 -0
- package/src/cli/ink-react.ts +26 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +6 -6
- package/src/components/layout/AppShell.tsx +8 -3
- package/src/components/projects/HomeChat.tsx +85 -48
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/ProjectInstructions.tsx +27 -38
- package/src/components/projects/ProjectSidePanel.tsx +382 -316
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentPicker.tsx +13 -4
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +27 -7
- package/src/components/sessions/ContextPanel.tsx +159 -59
- package/src/components/sessions/ModelPicker.tsx +65 -27
- package/src/components/sessions/ModelSearchList.tsx +137 -57
- package/src/components/sessions/NewSessionDialog.tsx +100 -117
- package/src/components/sessions/SessionComposer.tsx +2 -1
- package/src/components/sessions/SessionHeader.tsx +1 -0
- package/src/components/sessions/SessionList.tsx +173 -66
- package/src/components/sessions/SessionRow.tsx +176 -76
- package/src/components/sessions/SessionView.tsx +17 -13
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +36 -0
- package/src/components/settings/LhrRemoteAccess.tsx +20 -25
- package/src/components/settings/RemoteAccessGroup.tsx +242 -267
- package/src/components/settings/prefs.tsx +69 -1
- package/src/components/skills/InstallNotifications.tsx +4 -3
- package/src/components/skills/InstallSkillDialog.tsx +256 -99
- package/src/components/skills/SkillCards.tsx +52 -8
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/config.ts +32 -3
- package/src/hooks/useAgentPicker.ts +6 -2
- package/src/hooks/useSessionList.ts +95 -2
- package/src/lib/__tests__/project-extensions.test.ts +32 -1
- package/src/lib/__tests__/project-overview.test.ts +15 -0
- package/src/lib/__tests__/routes.test.ts +2 -2
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/skill-installs.test.ts +1 -0
- package/src/lib/__tests__/skills-catalog.test.ts +36 -8
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/project-extensions.ts +66 -4
- package/src/lib/project-overview.ts +14 -8
- package/src/lib/routes.ts +13 -4
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/skill-installs.ts +3 -1
- package/src/lib/skills-catalog.ts +56 -17
- package/src/pages/projects/ProjectDetailPage.tsx +13 -12
- package/src/pages/settings/SettingsPage.tsx +212 -145
- package/src/pages/settings/__tests__/SettingsPage.test.ts +11 -4
- package/src/pages/skills/SkillsPage.tsx +89 -45
- package/src/server/__tests__/app.test.ts +15 -2
- package/src/server/app.ts +13 -1
- package/src/server/routes/projects.routes.ts +14 -4
- package/src/server/routes/sessions.routes.ts +1 -1
- package/src/server/routes/skills.routes.ts +24 -5
- package/src/server/services/__tests__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -0
- package/src/server/services/__tests__/session-manager.test.ts +40 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +22 -3
- package/src/server/services/__tests__/skills-registry.test.ts +6 -0
- package/src/server/services/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -0
- package/src/server/services/session-manager.ts +117 -52
- package/src/server/services/skill-install-jobs.ts +21 -8
- package/src/server/services/skills-registry.ts +18 -6
- package/src/types/message.ts +1 -1
- package/src/components/projects/PanelSection.tsx +0 -102
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* diklik (tombol / tautan) dengan chevron seperti GNOME.
|
|
7
7
|
* - `EntryRow` label kecil di atas input sebaris; tombol ✓ muncul saat
|
|
8
8
|
* nilai berubah (seperti AdwEntryRow "apply button").
|
|
9
|
+
* - `ExpanderRow` baris yang membuka isi tambahan di bawahnya (seperti
|
|
10
|
+
* AdwExpanderRow): chevron berputar, isi berlatar redup.
|
|
9
11
|
* - `SwitchRow` tidak dibutuhkan saat ini; `ComboRow` diwakili ActionRow +
|
|
10
12
|
* Select di suffix.
|
|
11
13
|
*/
|
|
12
|
-
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
|
14
|
+
import { CheckIcon, ChevronDownIcon, ChevronRightIcon } from "lucide-react";
|
|
13
15
|
import { type ReactNode, useId, useState } from "react";
|
|
14
16
|
import { Spinner } from "@/components/ui/spinner";
|
|
15
17
|
import { cn } from "@/lib/utils";
|
|
@@ -19,6 +21,7 @@ export function PrefsGroup({
|
|
|
19
21
|
title,
|
|
20
22
|
description,
|
|
21
23
|
suffix,
|
|
24
|
+
toolbar,
|
|
22
25
|
children,
|
|
23
26
|
}: {
|
|
24
27
|
id?: string;
|
|
@@ -26,6 +29,8 @@ export function PrefsGroup({
|
|
|
26
29
|
description?: ReactNode;
|
|
27
30
|
/** Aksi kecil di kanan judul grup (mis. tombol). */
|
|
28
31
|
suffix?: ReactNode;
|
|
32
|
+
/** Kontrol di antara judul & boxed list (mis. kolom cari, filter). */
|
|
33
|
+
toolbar?: ReactNode;
|
|
29
34
|
children: ReactNode;
|
|
30
35
|
}) {
|
|
31
36
|
const headingId = useId();
|
|
@@ -50,6 +55,7 @@ export function PrefsGroup({
|
|
|
50
55
|
{description && <p className="text-[13px] text-muted-foreground">{description}</p>}
|
|
51
56
|
</div>
|
|
52
57
|
)}
|
|
58
|
+
{toolbar}
|
|
53
59
|
<div className="overflow-hidden rounded-xl border bg-card shadow-xs">
|
|
54
60
|
<ul className="flex flex-col divide-y">{children}</ul>
|
|
55
61
|
</div>
|
|
@@ -242,6 +248,68 @@ export function EntryRow({
|
|
|
242
248
|
);
|
|
243
249
|
}
|
|
244
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Baris ala AdwExpanderRow: judul + subjudul (1 baris), suffix, chevron;
|
|
253
|
+
* klik membuka `children` di bawahnya (latar redup, dipisah garis). Tanpa
|
|
254
|
+
* `children` baris tampil biasa (tidak bisa dibuka).
|
|
255
|
+
*/
|
|
256
|
+
export function ExpanderRow({
|
|
257
|
+
title,
|
|
258
|
+
subtitle,
|
|
259
|
+
prefix,
|
|
260
|
+
suffix,
|
|
261
|
+
children,
|
|
262
|
+
className,
|
|
263
|
+
}: {
|
|
264
|
+
title: ReactNode;
|
|
265
|
+
subtitle?: ReactNode;
|
|
266
|
+
prefix?: ReactNode;
|
|
267
|
+
suffix?: ReactNode;
|
|
268
|
+
children?: ReactNode;
|
|
269
|
+
className?: string;
|
|
270
|
+
}) {
|
|
271
|
+
const [open, setOpen] = useState(false);
|
|
272
|
+
const contentId = useId();
|
|
273
|
+
const expandable = children !== undefined && children !== null && children !== false;
|
|
274
|
+
return (
|
|
275
|
+
<li className={className}>
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
onClick={() => expandable && setOpen((v) => !v)}
|
|
279
|
+
aria-expanded={expandable ? open : undefined}
|
|
280
|
+
aria-controls={expandable ? contentId : undefined}
|
|
281
|
+
disabled={!expandable}
|
|
282
|
+
className="flex min-h-14 w-full items-center gap-3 px-4 py-2.5 text-left transition-colors outline-none hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:ring-[3px] focus-visible:ring-ring/40 focus-visible:ring-inset active:bg-muted disabled:cursor-default disabled:hover:bg-transparent"
|
|
283
|
+
>
|
|
284
|
+
{prefix && <span className="flex shrink-0 items-center">{prefix}</span>}
|
|
285
|
+
<span className="flex min-w-0 flex-1 flex-col gap-0.5 py-0.5">
|
|
286
|
+
<span className="flex min-w-0 items-center gap-2 text-[15px] leading-5">{title}</span>
|
|
287
|
+
{subtitle && (
|
|
288
|
+
<span className="truncate text-[13px] leading-snug text-muted-foreground">
|
|
289
|
+
{subtitle}
|
|
290
|
+
</span>
|
|
291
|
+
)}
|
|
292
|
+
</span>
|
|
293
|
+
{suffix && <span className="flex shrink-0 items-center gap-2">{suffix}</span>}
|
|
294
|
+
{expandable && (
|
|
295
|
+
<ChevronDownIcon
|
|
296
|
+
className={cn(
|
|
297
|
+
"size-4 shrink-0 text-muted-foreground transition-transform duration-200",
|
|
298
|
+
open && "rotate-180",
|
|
299
|
+
)}
|
|
300
|
+
aria-hidden
|
|
301
|
+
/>
|
|
302
|
+
)}
|
|
303
|
+
</button>
|
|
304
|
+
{expandable && open && (
|
|
305
|
+
<div id={contentId} className="border-t bg-muted/30 px-4 py-3">
|
|
306
|
+
{children}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</li>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
245
313
|
/** Pil status kecil untuk suffix baris (mis. "On" / "Off"). */
|
|
246
314
|
export function RowBadge({
|
|
247
315
|
tone = "muted",
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "lucide-react";
|
|
25
25
|
import { useEffect, useRef, useState } from "react";
|
|
26
26
|
import { Spinner } from "@/components/ui/spinner";
|
|
27
|
+
import { AGENT_TYPE_LABELS } from "@/lib/agent-types";
|
|
27
28
|
import { apiErrorMessage } from "@/lib/api";
|
|
28
29
|
import {
|
|
29
30
|
cancelInstall,
|
|
@@ -123,9 +124,9 @@ function InstallCard({ entry }: { entry: InstallEntry }) {
|
|
|
123
124
|
? apiErrorMessage(job.error ?? "SKILL_INSTALL_FAILED")
|
|
124
125
|
: awaitingActivation
|
|
125
126
|
? `Added to ${job.projectName}. Activates automatically when the running chat finishes.`
|
|
126
|
-
: `${running ? "into" : "Added to"} ${job.projectName}${
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
: `${running ? "into" : "Added to"} ${job.projectName} · ${(job.agentTypes ?? ["opencode"])
|
|
128
|
+
.map((a) => AGENT_TYPE_LABELS[a])
|
|
129
|
+
.join(" + ")}${job.status === "succeeded" ? " · active" : ""}`;
|
|
129
130
|
|
|
130
131
|
return (
|
|
131
132
|
<article
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dialog konfirmasi instalasi skill dari skills.sh.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Susunan (atas -> bawah), dari yang paling sering diputuskan:
|
|
5
|
+
* 1. Identitas skill: avatar owner, nama, `source · installs · skills.sh ↗`.
|
|
6
|
+
* 2. Agent tujuan: dua tile berdampingan (OpenCode / Claude Code, bisa
|
|
7
|
+
* keduanya). Agent yang belum memuat skill ini terpilih secara default;
|
|
8
|
+
* yang sudah memuatnya ditandai "Installed" (memilihnya = pasang ulang).
|
|
9
|
+
* 3. Audit keamanan (`GET /api/skills/audit`) dalam satu baris ringkas;
|
|
10
|
+
* rincian per partner dilipat dan otomatis terbuka bila ada temuan
|
|
11
|
+
* (`warn`/`fail`). Skill `fail` tetap boleh dipasang, tombolnya destruktif.
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
* output CLI tampil di notifikasi
|
|
13
|
+
* Label tombol menyebut agent tujuan ("Install for Claude Code"). Konfirmasi
|
|
14
|
+
* hanya MEMULAI job instalasi; progres + output CLI tampil di notifikasi
|
|
15
|
+
* instalasi (`InstallNotifications`).
|
|
11
16
|
*/
|
|
12
17
|
import {
|
|
18
|
+
CheckIcon,
|
|
19
|
+
ChevronDownIcon,
|
|
13
20
|
ExternalLinkIcon,
|
|
14
21
|
ShieldAlertIcon,
|
|
15
22
|
ShieldCheckIcon,
|
|
16
23
|
ShieldQuestionIcon,
|
|
17
24
|
} from "lucide-react";
|
|
18
25
|
import { useEffect, useState } from "react";
|
|
26
|
+
import { AgentLogo } from "@/components/sessions/AgentLogo";
|
|
27
|
+
import { SourceAvatar } from "@/components/skills/SkillCards";
|
|
19
28
|
import {
|
|
20
29
|
AlertDialog,
|
|
21
30
|
AlertDialogAction,
|
|
@@ -26,12 +35,15 @@ import {
|
|
|
26
35
|
AlertDialogHeader,
|
|
27
36
|
AlertDialogTitle,
|
|
28
37
|
} from "@/components/ui/alert-dialog";
|
|
38
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
29
39
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
30
40
|
import { Spinner } from "@/components/ui/spinner";
|
|
41
|
+
import { AGENT_TYPE_LABELS, AGENT_TYPES } from "@/lib/agent-types";
|
|
31
42
|
import { ApiError, apiFetch } from "@/lib/api";
|
|
32
43
|
import { AUDIT_BADGE_CLASS, AUDIT_LABEL, formatInstalls } from "@/lib/skills-catalog";
|
|
33
44
|
import { cn } from "@/lib/utils";
|
|
34
45
|
import type { RegistrySkill, SkillAudit } from "@/server/services/skills-registry";
|
|
46
|
+
import type { AgentType } from "@/types";
|
|
35
47
|
|
|
36
48
|
type AuditState =
|
|
37
49
|
| { phase: "loading" }
|
|
@@ -42,23 +54,52 @@ export interface InstallSkillDialogProps {
|
|
|
42
54
|
/** Skill yang akan dipasang; `null` = dialog tertutup. */
|
|
43
55
|
skill: RegistrySkill | null;
|
|
44
56
|
projectName: string;
|
|
57
|
+
/** Agent yang sudah memuat skill ini dari folder Project. */
|
|
58
|
+
installedAgents: readonly AgentType[];
|
|
45
59
|
/** Permintaan memulai instalasi sedang dikirim. */
|
|
46
60
|
starting: boolean;
|
|
47
61
|
/** Pesan bila server menolak memulai (mis. instalasi lain sedang berjalan). */
|
|
48
62
|
startError: string | null;
|
|
49
63
|
onOpenChange: (open: boolean) => void;
|
|
50
|
-
onConfirm: (skill: RegistrySkill) => void;
|
|
64
|
+
onConfirm: (skill: RegistrySkill, agents: AgentType[]) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Folder skill di Project per agent (petunjuk di tile). */
|
|
68
|
+
const AGENT_SKILLS_DIR: Record<AgentType, string> = {
|
|
69
|
+
opencode: ".agents/skills",
|
|
70
|
+
"claude-code": ".claude/skills",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Label tombol utama sesuai agent terpilih. */
|
|
74
|
+
function installLabel(agents: readonly AgentType[], flagged: boolean): string {
|
|
75
|
+
const verb = flagged ? "Install anyway" : "Install";
|
|
76
|
+
if (agents.length === 0) return verb;
|
|
77
|
+
if (agents.length === AGENT_TYPES.length) return `${verb} for both`;
|
|
78
|
+
return `${verb} for ${agents.map((a) => AGENT_TYPE_LABELS[a]).join(" + ")}`;
|
|
51
79
|
}
|
|
52
80
|
|
|
53
81
|
export function InstallSkillDialog({
|
|
54
82
|
skill,
|
|
55
83
|
projectName,
|
|
84
|
+
installedAgents,
|
|
56
85
|
starting,
|
|
57
86
|
startError,
|
|
58
87
|
onOpenChange,
|
|
59
88
|
onConfirm,
|
|
60
89
|
}: InstallSkillDialogProps) {
|
|
61
90
|
const [audit, setAudit] = useState<AuditState>({ phase: "loading" });
|
|
91
|
+
const [agents, setAgents] = useState<AgentType[]>([]);
|
|
92
|
+
|
|
93
|
+
// Dialog dibuka untuk skill baru -> pilih agent yang belum memuatnya.
|
|
94
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: hanya saat skill berganti
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!skill) return;
|
|
97
|
+
const missing = AGENT_TYPES.filter((a) => !installedAgents.includes(a));
|
|
98
|
+
setAgents(missing.length > 0 ? missing : [...AGENT_TYPES]);
|
|
99
|
+
}, [skill]);
|
|
100
|
+
|
|
101
|
+
const toggleAgent = (agent: AgentType, on: boolean) =>
|
|
102
|
+
setAgents((prev) => AGENT_TYPES.filter((a) => (a === agent ? on : prev.includes(a))));
|
|
62
103
|
|
|
63
104
|
useEffect(() => {
|
|
64
105
|
if (!skill) return;
|
|
@@ -92,45 +133,68 @@ export function InstallSkillDialog({
|
|
|
92
133
|
if (!starting) onOpenChange(next);
|
|
93
134
|
}}
|
|
94
135
|
>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
136
|
+
{/* Tinggi dibatasi agar dialog (audit terbuka) tetap muat di layar HP. */}
|
|
137
|
+
<AlertDialogContent className="max-h-[calc(100dvh-2rem)] gap-5 overflow-y-auto sm:max-w-md">
|
|
138
|
+
<AlertDialogHeader className="text-left">
|
|
139
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
140
|
+
{skill && <SourceAvatar source={skill.source} />}
|
|
141
|
+
<div className="flex min-w-0 flex-col gap-0.5">
|
|
142
|
+
<AlertDialogTitle className="truncate" title={skill?.name}>
|
|
143
|
+
Install {skill?.name}
|
|
144
|
+
</AlertDialogTitle>
|
|
145
|
+
{skill && (
|
|
146
|
+
<p className="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
|
|
147
|
+
<span className="truncate font-mono" title={skill.source}>
|
|
148
|
+
{skill.source}
|
|
149
|
+
</span>
|
|
150
|
+
<span aria-hidden>·</span>
|
|
151
|
+
<span className="shrink-0 tabular-nums">
|
|
152
|
+
{formatInstalls(skill.installs)} installs
|
|
153
|
+
</span>
|
|
154
|
+
<span aria-hidden>·</span>
|
|
155
|
+
<a
|
|
156
|
+
href={skill.url}
|
|
157
|
+
target="_blank"
|
|
158
|
+
rel="noopener noreferrer"
|
|
159
|
+
className="inline-flex shrink-0 items-center gap-0.5 font-medium text-foreground underline-offset-4 hover:underline"
|
|
160
|
+
>
|
|
161
|
+
skills.sh
|
|
162
|
+
<ExternalLinkIcon className="size-3" aria-hidden />
|
|
163
|
+
<span className="sr-only">(opens in a new tab)</span>
|
|
164
|
+
</a>
|
|
165
|
+
</p>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<AlertDialogDescription className="sr-only">
|
|
170
|
+
Choose which agents in {projectName} get this skill, then review its security audit.
|
|
102
171
|
</AlertDialogDescription>
|
|
103
172
|
</AlertDialogHeader>
|
|
104
173
|
|
|
105
174
|
{skill && (
|
|
106
|
-
<div className="flex flex-col gap-
|
|
107
|
-
<
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<AuditSummary state={audit} />
|
|
129
|
-
</section>
|
|
175
|
+
<div className="flex min-w-0 flex-col gap-4 text-sm">
|
|
176
|
+
<fieldset className="flex flex-col gap-2" disabled={starting}>
|
|
177
|
+
<legend className="mb-2 text-xs text-muted-foreground">
|
|
178
|
+
Add to <span className="font-medium text-foreground">{projectName}</span> for
|
|
179
|
+
</legend>
|
|
180
|
+
<div className="grid grid-cols-2 gap-2">
|
|
181
|
+
{AGENT_TYPES.map((a) => (
|
|
182
|
+
<AgentTile
|
|
183
|
+
key={a}
|
|
184
|
+
agent={a}
|
|
185
|
+
checked={agents.includes(a)}
|
|
186
|
+
installed={installedAgents.includes(a)}
|
|
187
|
+
onChange={(on) => toggleAgent(a, on)}
|
|
188
|
+
/>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
{agents.length === 0 && (
|
|
192
|
+
<p className="text-xs text-muted-foreground">Pick at least one agent.</p>
|
|
193
|
+
)}
|
|
194
|
+
</fieldset>
|
|
195
|
+
|
|
196
|
+
<AuditPanel state={audit} />
|
|
130
197
|
|
|
131
|
-
<p className="text-xs text-muted-foreground">
|
|
132
|
-
Installation runs in the background. You can follow the output in the notification.
|
|
133
|
-
</p>
|
|
134
198
|
{startError && (
|
|
135
199
|
<p role="alert" className="text-xs text-destructive">
|
|
136
200
|
{startError}
|
|
@@ -139,64 +203,148 @@ export function InstallSkillDialog({
|
|
|
139
203
|
</div>
|
|
140
204
|
)}
|
|
141
205
|
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
206
|
+
<div className="flex flex-col gap-3">
|
|
207
|
+
<p className="text-xs text-muted-foreground">
|
|
208
|
+
Skills run with the agent's full permissions. Installation continues in the background —
|
|
209
|
+
follow it in the notification.
|
|
210
|
+
</p>
|
|
211
|
+
<AlertDialogFooter>
|
|
212
|
+
<AlertDialogCancel disabled={starting}>Cancel</AlertDialogCancel>
|
|
213
|
+
<AlertDialogAction
|
|
214
|
+
variant={flagged ? "destructive" : "default"}
|
|
215
|
+
disabled={starting || !skill || agents.length === 0}
|
|
216
|
+
onClick={(e) => {
|
|
217
|
+
// Dialog tetap terbuka sampai job berhasil dibuat.
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
if (skill && agents.length > 0) onConfirm(skill, agents);
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
{starting ? (
|
|
223
|
+
<>
|
|
224
|
+
<Spinner data-icon="inline-start" />
|
|
225
|
+
Starting…
|
|
226
|
+
</>
|
|
227
|
+
) : (
|
|
228
|
+
installLabel(agents, flagged)
|
|
229
|
+
)}
|
|
230
|
+
</AlertDialogAction>
|
|
231
|
+
</AlertDialogFooter>
|
|
232
|
+
</div>
|
|
165
233
|
</AlertDialogContent>
|
|
166
234
|
</AlertDialog>
|
|
167
235
|
);
|
|
168
236
|
}
|
|
169
237
|
|
|
170
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Tile agent yang bisa dipilih: logo, nama, folder tujuan, indikator centang
|
|
240
|
+
* di pojok. Checkbox asli disembunyikan (`sr-only`) tapi tetap jadi kontrol
|
|
241
|
+
* yang dibaca screen reader & menerima fokus keyboard (Spasi).
|
|
242
|
+
*/
|
|
243
|
+
function AgentTile({
|
|
244
|
+
agent,
|
|
245
|
+
checked,
|
|
246
|
+
installed,
|
|
247
|
+
onChange,
|
|
248
|
+
}: {
|
|
249
|
+
agent: AgentType;
|
|
250
|
+
checked: boolean;
|
|
251
|
+
installed: boolean;
|
|
252
|
+
onChange: (checked: boolean) => void;
|
|
253
|
+
}) {
|
|
254
|
+
return (
|
|
255
|
+
<label
|
|
256
|
+
className={cn(
|
|
257
|
+
"relative flex cursor-pointer flex-col gap-2.5 rounded-xl border p-3 transition-colors select-none",
|
|
258
|
+
"has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50 has-disabled:cursor-not-allowed has-disabled:opacity-60",
|
|
259
|
+
checked ? "border-primary bg-primary/5" : "hover:border-foreground/20 hover:bg-accent/30",
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
<input
|
|
263
|
+
type="checkbox"
|
|
264
|
+
checked={checked}
|
|
265
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
266
|
+
className="peer sr-only"
|
|
267
|
+
/>
|
|
268
|
+
<span className="flex items-start justify-between">
|
|
269
|
+
<span className="flex size-8 items-center justify-center rounded-lg bg-muted text-foreground">
|
|
270
|
+
<AgentLogo agentType={agent} className="size-4" />
|
|
271
|
+
</span>
|
|
272
|
+
<span
|
|
273
|
+
aria-hidden
|
|
274
|
+
className={cn(
|
|
275
|
+
"flex size-5 items-center justify-center rounded-full border transition-colors",
|
|
276
|
+
checked ? "border-primary bg-primary text-primary-foreground" : "bg-background",
|
|
277
|
+
)}
|
|
278
|
+
>
|
|
279
|
+
{checked && <CheckIcon className="size-3" strokeWidth={3} />}
|
|
280
|
+
</span>
|
|
281
|
+
</span>
|
|
282
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
283
|
+
<span className="text-sm font-medium leading-5">{AGENT_TYPE_LABELS[agent]}</span>
|
|
284
|
+
<span className="truncate font-mono text-[11px] text-muted-foreground">
|
|
285
|
+
{installed ? (checked ? "Installed · reinstall" : "Installed") : AGENT_SKILLS_DIR[agent]}
|
|
286
|
+
</span>
|
|
287
|
+
</span>
|
|
288
|
+
</label>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Audit keamanan sebagai satu baris ringkas (ikon, status, jumlah partner
|
|
294
|
+
* yang lolos). Rincian per partner bisa dibuka; terbuka otomatis bila ada
|
|
295
|
+
* temuan agar tidak terlewat.
|
|
296
|
+
*/
|
|
297
|
+
function AuditPanel({ state }: { state: AuditState }) {
|
|
298
|
+
const shell = "rounded-xl border bg-muted/30";
|
|
299
|
+
|
|
171
300
|
if (state.phase === "loading") {
|
|
172
301
|
return (
|
|
173
|
-
<div role="status" className="flex
|
|
302
|
+
<div role="status" aria-busy="true" className={cn(shell, "flex items-center gap-3 p-3")}>
|
|
174
303
|
<span className="sr-only">Loading security audit…</span>
|
|
175
|
-
<Skeleton className="
|
|
176
|
-
<Skeleton className="h-
|
|
304
|
+
<Skeleton className="size-4 rounded-full" />
|
|
305
|
+
<Skeleton className="h-4 w-40" />
|
|
177
306
|
</div>
|
|
178
307
|
);
|
|
179
308
|
}
|
|
180
309
|
if (state.phase === "error") {
|
|
181
|
-
return
|
|
310
|
+
return (
|
|
311
|
+
<p className={cn(shell, "flex items-center gap-2 p-3 text-xs text-muted-foreground")}>
|
|
312
|
+
<ShieldQuestionIcon className="size-4 shrink-0" aria-hidden />
|
|
313
|
+
{state.message}
|
|
314
|
+
</p>
|
|
315
|
+
);
|
|
182
316
|
}
|
|
183
317
|
|
|
184
318
|
const { audits, overall } = state.audit;
|
|
185
319
|
if (audits === null || overall === null) {
|
|
186
320
|
return (
|
|
187
|
-
<p className="flex items-
|
|
321
|
+
<p className={cn(shell, "flex items-start gap-2 p-3 text-xs text-muted-foreground")}>
|
|
188
322
|
<ShieldQuestionIcon className="size-4 shrink-0" aria-hidden />
|
|
189
|
-
|
|
323
|
+
<span>
|
|
324
|
+
<span className="font-medium text-foreground">Not audited yet.</span> Review the skill on
|
|
325
|
+
skills.sh before installing.
|
|
326
|
+
</span>
|
|
190
327
|
</p>
|
|
191
328
|
);
|
|
192
329
|
}
|
|
193
330
|
|
|
331
|
+
const passed = audits.filter((a) => a.status === "pass").length;
|
|
194
332
|
const Icon = overall === "pass" ? ShieldCheckIcon : ShieldAlertIcon;
|
|
195
333
|
return (
|
|
196
|
-
|
|
197
|
-
<
|
|
198
|
-
<Icon
|
|
199
|
-
|
|
334
|
+
<Collapsible defaultOpen={overall !== "pass"} className={shell}>
|
|
335
|
+
<CollapsibleTrigger className="group flex w-full items-center gap-2 rounded-xl p-3 text-left text-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50">
|
|
336
|
+
<Icon
|
|
337
|
+
className={cn(
|
|
338
|
+
"size-4 shrink-0",
|
|
339
|
+
overall === "pass"
|
|
340
|
+
? "text-emerald-600 dark:text-emerald-400"
|
|
341
|
+
: overall === "warn"
|
|
342
|
+
? "text-amber-600 dark:text-amber-400"
|
|
343
|
+
: "text-destructive",
|
|
344
|
+
)}
|
|
345
|
+
aria-hidden
|
|
346
|
+
/>
|
|
347
|
+
<span className="font-medium">Security audit</span>
|
|
200
348
|
<span
|
|
201
349
|
className={cn(
|
|
202
350
|
"rounded px-1.5 text-[11px] leading-5 font-medium",
|
|
@@ -205,29 +353,38 @@ function AuditSummary({ state }: { state: AuditState }) {
|
|
|
205
353
|
>
|
|
206
354
|
{AUDIT_LABEL[overall]}
|
|
207
355
|
</span>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
356
|
+
<span className="ml-auto text-muted-foreground tabular-nums">
|
|
357
|
+
{passed}/{audits.length} passed
|
|
358
|
+
</span>
|
|
359
|
+
<ChevronDownIcon
|
|
360
|
+
className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180"
|
|
361
|
+
aria-hidden
|
|
362
|
+
/>
|
|
363
|
+
</CollapsibleTrigger>
|
|
364
|
+
<CollapsibleContent>
|
|
365
|
+
<ul className="flex flex-col gap-2 border-t px-3 py-3">
|
|
366
|
+
{audits.map((a) => (
|
|
367
|
+
<li key={a.provider} className="flex items-start justify-between gap-3 text-xs">
|
|
368
|
+
<span className="flex min-w-0 flex-col">
|
|
369
|
+
<span className="font-medium">{a.provider}</span>
|
|
370
|
+
{a.summary && (
|
|
371
|
+
<span className="line-clamp-2 text-muted-foreground" title={a.summary}>
|
|
372
|
+
{a.summary}
|
|
373
|
+
</span>
|
|
374
|
+
)}
|
|
375
|
+
</span>
|
|
376
|
+
<span
|
|
377
|
+
className={cn(
|
|
378
|
+
"shrink-0 rounded px-1.5 text-[11px] leading-5 font-medium",
|
|
379
|
+
AUDIT_BADGE_CLASS[a.status],
|
|
380
|
+
)}
|
|
381
|
+
>
|
|
382
|
+
{AUDIT_LABEL[a.status]}
|
|
383
|
+
</span>
|
|
384
|
+
</li>
|
|
385
|
+
))}
|
|
386
|
+
</ul>
|
|
387
|
+
</CollapsibleContent>
|
|
388
|
+
</Collapsible>
|
|
232
389
|
);
|
|
233
390
|
}
|