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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logo backend Session (OpenCode / Claude Code) untuk pemilih agent.
|
|
3
|
+
*
|
|
4
|
+
* - OpenCode : tanda "frame" dari favicon resmi opencode.ai, digambar ulang
|
|
5
|
+
* dengan `currentColor` agar mengikuti tema terang/gelap.
|
|
6
|
+
* - Claude : tanda Claude (simple-icons) dengan warna brand #D97757.
|
|
7
|
+
*/
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import type { AgentType } from "@/types";
|
|
10
|
+
|
|
11
|
+
const CLAUDE_PATH =
|
|
12
|
+
"m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z";
|
|
13
|
+
|
|
14
|
+
export function AgentLogo({ agentType, className }: { agentType: AgentType; className?: string }) {
|
|
15
|
+
if (agentType === "claude-code") {
|
|
16
|
+
return (
|
|
17
|
+
<svg
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
aria-hidden="true"
|
|
20
|
+
className={cn("size-4 shrink-0", className)}
|
|
21
|
+
fill="#D97757"
|
|
22
|
+
>
|
|
23
|
+
<path d={CLAUDE_PATH} />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return (
|
|
28
|
+
<svg
|
|
29
|
+
viewBox="96 96 320 320"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
className={cn("size-4 shrink-0", className)}
|
|
32
|
+
fill="currentColor"
|
|
33
|
+
>
|
|
34
|
+
<path d="M320 224V352H192V224H320Z" opacity={0.4} />
|
|
35
|
+
<path
|
|
36
|
+
fillRule="evenodd"
|
|
37
|
+
clipRule="evenodd"
|
|
38
|
+
d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z"
|
|
39
|
+
/>
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -22,9 +22,12 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|
|
22
22
|
import { Spinner } from "@/components/ui/spinner";
|
|
23
23
|
import { useAgentPicker } from "@/hooks/useAgentPicker";
|
|
24
24
|
import { cn } from "@/lib/utils";
|
|
25
|
+
import type { AgentType } from "@/types";
|
|
25
26
|
|
|
26
27
|
export interface AgentPickerProps {
|
|
27
28
|
projectId: string;
|
|
29
|
+
/** Backend Session — Claude Code menawarkan mode permission. */
|
|
30
|
+
agentType?: AgentType;
|
|
28
31
|
/** null = Session belum ada (composer homepage): pilihan tidak di-PUT. */
|
|
29
32
|
sessionId: string | null;
|
|
30
33
|
/** Agent aktif saat ini; null = default opencode (build). */
|
|
@@ -39,6 +42,7 @@ export interface AgentPickerProps {
|
|
|
39
42
|
|
|
40
43
|
export function AgentPicker({
|
|
41
44
|
projectId,
|
|
45
|
+
agentType = "opencode",
|
|
42
46
|
sessionId,
|
|
43
47
|
agent,
|
|
44
48
|
onChanged,
|
|
@@ -53,6 +57,7 @@ export function AgentPicker({
|
|
|
53
57
|
onChanged(name);
|
|
54
58
|
setOpen(false);
|
|
55
59
|
},
|
|
60
|
+
agentType,
|
|
56
61
|
);
|
|
57
62
|
|
|
58
63
|
const isPlan = agent?.toLowerCase() === "plan";
|
|
@@ -99,14 +104,18 @@ export function AgentPicker({
|
|
|
99
104
|
side="top"
|
|
100
105
|
sideOffset={8}
|
|
101
106
|
collisionPadding={12}
|
|
102
|
-
|
|
107
|
+
// Tinggi SELURUH popover (header + daftar) dibatasi ruang yang
|
|
108
|
+
// tersedia (Radix). Bila hanya daftar yang dibatasi, header membuat
|
|
109
|
+
// popover selalu sedikit melebihi ruang -> floating-ui membalik sisi
|
|
110
|
+
// atas/bawah, tinggi daftar ikut berubah, dan ResizeObserver berputar
|
|
111
|
+
// ("ResizeObserver loop completed…") saat "More modes" dibuka.
|
|
112
|
+
className="flex max-h-[min(26rem,var(--radix-popover-content-available-height))] w-80 flex-col overflow-hidden p-0"
|
|
103
113
|
>
|
|
104
|
-
<div className="border-b px-3 pt-2.5 pb-2">
|
|
114
|
+
<div className="shrink-0 border-b px-3 pt-2.5 pb-2">
|
|
105
115
|
<p className="text-sm font-medium">Mode</p>
|
|
106
116
|
<p className="text-xs text-muted-foreground">How the agent handles your next message</p>
|
|
107
117
|
</div>
|
|
108
|
-
|
|
109
|
-
<div className="max-h-[min(22rem,var(--radix-popover-content-available-height))] overflow-y-auto overscroll-contain p-1.5 [scrollbar-width:thin]">
|
|
118
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-1.5 [scrollbar-width:thin]">
|
|
110
119
|
<AgentModeList
|
|
111
120
|
agents={agents}
|
|
112
121
|
loading={loading}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Konfirmasi hapus Session permanen (dipakai header chat `SessionView
|
|
3
|
-
*
|
|
4
|
-
* opencode — tidak bisa
|
|
2
|
+
* Konfirmasi hapus Session permanen (dipakai header chat `SessionView`, menu
|
|
3
|
+
* baris `SessionList`, dan hapus massal selection mode). Menghapus juga
|
|
4
|
+
* riwayat percakapan di agent (opencode / Claude Code) — tidak bisa
|
|
5
|
+
* dibatalkan. `count` > 1 = hapus massal; `progress` menampilkan kemajuan.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { useState } from "react";
|
|
7
9
|
import {
|
|
8
10
|
AlertDialog,
|
|
9
11
|
AlertDialogAction,
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
AlertDialogTitle,
|
|
16
18
|
} from "@/components/ui/alert-dialog";
|
|
17
19
|
import { Spinner } from "@/components/ui/spinner";
|
|
20
|
+
import { pluralSessions } from "@/lib/session-selection";
|
|
18
21
|
|
|
19
22
|
export interface ConfirmSessionDeleteDialogProps {
|
|
20
23
|
open: boolean;
|
|
@@ -23,6 +26,10 @@ export interface ConfirmSessionDeleteDialogProps {
|
|
|
23
26
|
deleting: boolean;
|
|
24
27
|
/** Jalankan penghapusan (engine). */
|
|
25
28
|
onConfirm: () => void;
|
|
29
|
+
/** Jumlah Session yang dihapus (default 1). */
|
|
30
|
+
count?: number;
|
|
31
|
+
/** Kemajuan hapus massal. */
|
|
32
|
+
progress?: { done: number; total: number } | null;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
export function ConfirmSessionDeleteDialog({
|
|
@@ -30,7 +37,14 @@ export function ConfirmSessionDeleteDialog({
|
|
|
30
37
|
onOpenChange,
|
|
31
38
|
deleting,
|
|
32
39
|
onConfirm,
|
|
40
|
+
count = 1,
|
|
41
|
+
progress = null,
|
|
33
42
|
}: ConfirmSessionDeleteDialogProps) {
|
|
43
|
+
// Jumlah dibekukan selama animasi tutup: setelah hapus massal pilihan
|
|
44
|
+
// sudah kosong, tapi teks dialog yang memudar tidak boleh berganti.
|
|
45
|
+
const [shownCount, setShownCount] = useState(count);
|
|
46
|
+
if (open && count !== shownCount) setShownCount(count);
|
|
47
|
+
const many = shownCount > 1;
|
|
34
48
|
return (
|
|
35
49
|
<AlertDialog
|
|
36
50
|
open={open}
|
|
@@ -40,10 +54,12 @@ export function ConfirmSessionDeleteDialog({
|
|
|
40
54
|
>
|
|
41
55
|
<AlertDialogContent>
|
|
42
56
|
<AlertDialogHeader>
|
|
43
|
-
<AlertDialogTitle>
|
|
57
|
+
<AlertDialogTitle>
|
|
58
|
+
{many ? `Delete ${pluralSessions(shownCount)}?` : "Delete this session?"}
|
|
59
|
+
</AlertDialogTitle>
|
|
44
60
|
<AlertDialogDescription>
|
|
45
|
-
|
|
46
|
-
action cannot be undone.
|
|
61
|
+
{many ? "Their conversation histories" : "The conversation history"} will also be
|
|
62
|
+
permanently deleted from the agent. This action cannot be undone.
|
|
47
63
|
</AlertDialogDescription>
|
|
48
64
|
</AlertDialogHeader>
|
|
49
65
|
<AlertDialogFooter>
|
|
@@ -59,8 +75,12 @@ export function ConfirmSessionDeleteDialog({
|
|
|
59
75
|
{deleting ? (
|
|
60
76
|
<>
|
|
61
77
|
<Spinner data-icon="inline-start" />
|
|
62
|
-
|
|
78
|
+
{progress && progress.total > 1
|
|
79
|
+
? `Deleting ${Math.min(progress.done + 1, progress.total)} of ${progress.total}…`
|
|
80
|
+
: "Deleting…"}
|
|
63
81
|
</>
|
|
82
|
+
) : many ? (
|
|
83
|
+
`Delete ${pluralSessions(shownCount)}`
|
|
64
84
|
) : (
|
|
65
85
|
"Delete permanently"
|
|
66
86
|
)}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Kartu konteks Session — keadaan kerja agent,
|
|
3
|
-
*
|
|
4
|
-
* kartu
|
|
2
|
+
* Kartu konteks Session — keadaan kerja agent, sebagai kolom di kanan
|
|
3
|
+
* percakapan (desktop). Kolom ini ikut tata letak: chat menyempit saat kartu
|
|
4
|
+
* dibuka, sehingga pesan & input tidak pernah tertutup kartu.
|
|
5
5
|
*
|
|
6
6
|
* Susunan (dari yang paling sering dilirik ke yang paling jarang):
|
|
7
7
|
* - **Environment** : perubahan file (+/-), cabang git, status push/pull.
|
|
8
|
-
* - **MCP** : server MCP Project; ringkasan "x of y connected" di
|
|
9
|
-
* judul supaya status terbaca tanpa membuka bagiannya.
|
|
10
8
|
* - **Task list** : `todowrite` agent + progres "selesai / total".
|
|
9
|
+
* - **MCP** : server MCP; ringkasan "x/y connected" di judul. Yang
|
|
10
|
+
* gagal & tersambung tampil per baris; yang perlu login
|
|
11
|
+
* (mis. puluhan konektor claude.ai) dan yang tidak aktif
|
|
12
|
+
* DILIPAT jadi satu baris "N need sign-in" agar tidak
|
|
13
|
+
* menenggelamkan bagian lain.
|
|
11
14
|
* - **Reference** : file project yang DIBACA agent (bukan yang ditulis),
|
|
12
15
|
* terbaru di atas; 8 pertama tampil, sisanya "Show N more".
|
|
13
16
|
*
|
|
14
|
-
* Pola UX:
|
|
15
|
-
* - Tiap bagian = judul yang bisa dilipat + ringkasan di kanan judul,
|
|
16
|
-
* kartu
|
|
17
|
-
*
|
|
18
|
-
* solid) menjadi penanda, dipisah garis tipis antar bagian.
|
|
17
|
+
* Pola UX (gaya libadwaita):
|
|
18
|
+
* - Tiap bagian = judul yang bisa dilipat + ringkasan di kanan judul, isi
|
|
19
|
+
* berupa "boxed list" (kartu membulat, baris berpemisah) di atas latar
|
|
20
|
+
* kartu yang lebih gelap — seperti AdwPreferencesGroup.
|
|
19
21
|
* - Status diwakili titik berwarna + label, bukan warna saja (a11y).
|
|
20
22
|
* - Buka/tutup: `ContextPanelTrigger` (hamburger) HILANG saat kartu terbuka;
|
|
21
23
|
* penutupnya tombol X di kartu. Esc juga menutup.
|
|
@@ -97,21 +99,16 @@ function ContextSections({
|
|
|
97
99
|
}: Omit<ContextPanelProps, "onClose"> & { reserveClose?: boolean }) {
|
|
98
100
|
if (loading && context === null) return <CardSkeleton />;
|
|
99
101
|
return (
|
|
100
|
-
<div
|
|
101
|
-
className={cn(
|
|
102
|
-
"flex flex-col divide-y [&>*]:py-2.5 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0.5",
|
|
103
|
-
reserveClose && "[&>*:first-child>button]:pe-7",
|
|
104
|
-
)}
|
|
105
|
-
>
|
|
102
|
+
<div className={cn("flex flex-col gap-4", reserveClose && "[&>*:first-child>button]:pe-7")}>
|
|
106
103
|
<EnvironmentSection context={context} />
|
|
107
|
-
<McpSection servers={context?.mcp ?? []} live={context?.live ?? false} />
|
|
108
104
|
<TaskSection todos={context?.todos ?? []} live={context?.live ?? false} />
|
|
105
|
+
<McpSection servers={context?.mcp ?? []} live={context?.live ?? false} />
|
|
109
106
|
<ReferenceSection references={references} />
|
|
110
107
|
</div>
|
|
111
108
|
);
|
|
112
109
|
}
|
|
113
110
|
|
|
114
|
-
/** Kartu desktop:
|
|
111
|
+
/** Kartu desktop: kolom kanan, berdampingan dengan percakapan. */
|
|
115
112
|
export function ContextPanel({ context, references, loading, onClose }: ContextPanelProps) {
|
|
116
113
|
// Esc menutup kartu (pola sama dengan popover/sheet aplikasi).
|
|
117
114
|
useEffect(() => {
|
|
@@ -128,7 +125,9 @@ export function ContextPanel({ context, references, loading, onClose }: ContextP
|
|
|
128
125
|
initial={{ opacity: 0, x: 10 }}
|
|
129
126
|
animate={{ opacity: 1, x: 0 }}
|
|
130
127
|
transition={{ type: "spring", visualDuration: 0.25, bounce: 0.1 }}
|
|
131
|
-
|
|
128
|
+
// Latar kartu = latar jendela (lebih gelap), boxed list di atasnya
|
|
129
|
+
// memakai `bg-card` — kontras ala AdwPreferencesPage.
|
|
130
|
+
className="relative flex max-h-full flex-col overflow-hidden rounded-2xl border bg-background shadow-xs"
|
|
132
131
|
>
|
|
133
132
|
{/* Tanpa judul kartu: judul tiap bagian sudah menjadi penanda. Tombol
|
|
134
133
|
tutup mengapung di pojok kanan atas, sejajar judul bagian pertama. */}
|
|
@@ -144,7 +143,7 @@ export function ContextPanel({ context, references, loading, onClose }: ContextP
|
|
|
144
143
|
<XIcon />
|
|
145
144
|
</Button>
|
|
146
145
|
|
|
147
|
-
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-2 pt-
|
|
146
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-2.5 pt-2 pb-3 [scrollbar-width:thin]">
|
|
148
147
|
<ContextSections context={context} references={references} loading={loading} reserveClose />
|
|
149
148
|
</div>
|
|
150
149
|
</m.aside>
|
|
@@ -224,29 +223,34 @@ function EnvironmentSection({ context }: { context: SessionContext | null }) {
|
|
|
224
223
|
);
|
|
225
224
|
}
|
|
226
225
|
|
|
227
|
-
/**
|
|
228
|
-
const
|
|
229
|
-
failed: 0,
|
|
230
|
-
needs_auth: 1,
|
|
231
|
-
needs_client_registration: 2,
|
|
232
|
-
connected: 3,
|
|
233
|
-
unknown: 4,
|
|
234
|
-
disabled: 5,
|
|
235
|
-
};
|
|
226
|
+
/** Status yang "perlu login" — dilipat jadi satu baris bila lebih dari satu. */
|
|
227
|
+
const NEEDS_SIGN_IN: McpServerState["status"][] = ["needs_auth", "needs_client_registration"];
|
|
236
228
|
|
|
237
229
|
function McpSection({ servers, live }: { servers: McpServerState[]; live: boolean }) {
|
|
238
230
|
const connected = servers.filter((s) => s.status === "connected").length;
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
231
|
+
const failed = servers.filter((s) => s.status === "failed").length;
|
|
232
|
+
const byName = (a: McpServerState, b: McpServerState) => a.name.localeCompare(b.name);
|
|
233
|
+
const signIn = servers.filter((s) => NEEDS_SIGN_IN.includes(s.status)).sort(byName);
|
|
234
|
+
const inactive = servers
|
|
235
|
+
.filter((s) => s.status === "disabled" || s.status === "unknown")
|
|
236
|
+
.sort(byName);
|
|
237
|
+
// Baris individual: gagal (paling atas) lalu tersambung. Kelompok berisi
|
|
238
|
+
// satu server tetap tampil sebagai baris biasa (tidak perlu dilipat).
|
|
239
|
+
const individual = [
|
|
240
|
+
...servers.filter((s) => s.status === "failed").sort(byName),
|
|
241
|
+
...servers.filter((s) => s.status === "connected").sort(byName),
|
|
242
|
+
...(signIn.length === 1 ? signIn : []),
|
|
243
|
+
...(inactive.length === 1 ? inactive : []),
|
|
244
|
+
];
|
|
243
245
|
|
|
244
246
|
return (
|
|
245
247
|
<Section
|
|
246
248
|
title="MCP"
|
|
247
249
|
summary={
|
|
248
250
|
servers.length > 0 ? (
|
|
249
|
-
|
|
251
|
+
// Hanya kegagalan yang diberi warna peringatan — konektor yang
|
|
252
|
+
// belum login itu normal, bukan masalah.
|
|
253
|
+
<span className={cn(failed > 0 && "text-amber-700 dark:text-amber-400")}>
|
|
250
254
|
{connected}/{servers.length} connected
|
|
251
255
|
</span>
|
|
252
256
|
) : undefined
|
|
@@ -256,30 +260,104 @@ function McpSection({ servers, live }: { servers: McpServerState[]; live: boolea
|
|
|
256
260
|
<Empty>{live ? "No MCP servers configured" : "Start the session to see MCP servers"}</Empty>
|
|
257
261
|
) : (
|
|
258
262
|
<Box>
|
|
259
|
-
{
|
|
260
|
-
<
|
|
261
|
-
key={server.name}
|
|
262
|
-
icon={<PlugIcon />}
|
|
263
|
-
label={server.name}
|
|
264
|
-
// Pesan error lengkap terbaca lewat `title` (tanpa memanjangkan baris).
|
|
265
|
-
title={server.error ?? undefined}
|
|
266
|
-
value={
|
|
267
|
-
<span className="flex items-center gap-1.5 text-[11.5px] text-muted-foreground">
|
|
268
|
-
<span
|
|
269
|
-
aria-hidden
|
|
270
|
-
className={cn("size-1.5 rounded-full", MCP_STATUS_DOT[server.status])}
|
|
271
|
-
/>
|
|
272
|
-
{MCP_STATUS_LABEL[server.status]}
|
|
273
|
-
</span>
|
|
274
|
-
}
|
|
275
|
-
/>
|
|
263
|
+
{individual.map((server) => (
|
|
264
|
+
<McpRow key={server.name} server={server} />
|
|
276
265
|
))}
|
|
266
|
+
{signIn.length > 1 && (
|
|
267
|
+
<McpGroupRow
|
|
268
|
+
label={`${signIn.length} need sign-in`}
|
|
269
|
+
dotClass={MCP_STATUS_DOT.needs_auth}
|
|
270
|
+
servers={signIn}
|
|
271
|
+
/>
|
|
272
|
+
)}
|
|
273
|
+
{inactive.length > 1 && (
|
|
274
|
+
<McpGroupRow
|
|
275
|
+
label={`${inactive.length} inactive`}
|
|
276
|
+
dotClass={MCP_STATUS_DOT.disabled}
|
|
277
|
+
servers={inactive}
|
|
278
|
+
/>
|
|
279
|
+
)}
|
|
277
280
|
</Box>
|
|
278
281
|
)}
|
|
279
282
|
</Section>
|
|
280
283
|
);
|
|
281
284
|
}
|
|
282
285
|
|
|
286
|
+
/** Satu server MCP: nama + status (titik + label); error di baris kedua. */
|
|
287
|
+
function McpRow({ server }: { server: McpServerState }) {
|
|
288
|
+
return (
|
|
289
|
+
<Row
|
|
290
|
+
icon={<PlugIcon />}
|
|
291
|
+
label={server.name}
|
|
292
|
+
hint={server.error ?? undefined}
|
|
293
|
+
hintTone={server.error ? "error" : undefined}
|
|
294
|
+
// Pesan error lengkap terbaca lewat `title` (baris kedua terpotong).
|
|
295
|
+
title={server.error ?? undefined}
|
|
296
|
+
value={<StatusText status={server.status} />}
|
|
297
|
+
/>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function StatusText({ status }: { status: McpServerState["status"] }) {
|
|
302
|
+
return (
|
|
303
|
+
<span className="flex items-center gap-1.5 text-[11.5px] text-muted-foreground">
|
|
304
|
+
<span aria-hidden className={cn("size-1.5 rounded-full", MCP_STATUS_DOT[status])} />
|
|
305
|
+
{MCP_STATUS_LABEL[status]}
|
|
306
|
+
</span>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Baris kelompok yang bisa dibuka (AdwExpanderRow): "17 need sign-in" —
|
|
312
|
+
* isinya nama server, tanpa status berulang (sudah di judul baris).
|
|
313
|
+
*/
|
|
314
|
+
function McpGroupRow({
|
|
315
|
+
label,
|
|
316
|
+
dotClass,
|
|
317
|
+
servers,
|
|
318
|
+
}: {
|
|
319
|
+
label: string;
|
|
320
|
+
dotClass: string;
|
|
321
|
+
servers: McpServerState[];
|
|
322
|
+
}) {
|
|
323
|
+
const [open, setOpen] = useState(false);
|
|
324
|
+
return (
|
|
325
|
+
<li>
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={() => setOpen((v) => !v)}
|
|
329
|
+
aria-expanded={open}
|
|
330
|
+
className="flex min-h-9 w-full min-w-0 items-center gap-2 px-2.5 py-1.5 text-left text-[12.5px] outline-none transition-colors hover:bg-muted/50 focus-visible:bg-muted/50"
|
|
331
|
+
>
|
|
332
|
+
<span aria-hidden className="flex w-3.5 shrink-0 justify-center">
|
|
333
|
+
<span className={cn("size-1.5 rounded-full", dotClass)} />
|
|
334
|
+
</span>
|
|
335
|
+
<span className="min-w-0 flex-1 truncate leading-5 text-muted-foreground">{label}</span>
|
|
336
|
+
<ChevronDownIcon
|
|
337
|
+
aria-hidden
|
|
338
|
+
className={cn(
|
|
339
|
+
"size-3.5 shrink-0 text-muted-foreground transition-transform duration-200",
|
|
340
|
+
open && "rotate-180",
|
|
341
|
+
)}
|
|
342
|
+
/>
|
|
343
|
+
</button>
|
|
344
|
+
{open && (
|
|
345
|
+
<ul className="border-t bg-muted/30 py-1">
|
|
346
|
+
{servers.map((s) => (
|
|
347
|
+
<li
|
|
348
|
+
key={s.name}
|
|
349
|
+
title={s.error ?? s.name}
|
|
350
|
+
className="truncate px-2.5 py-1 ps-8 text-[12px] leading-5 text-muted-foreground"
|
|
351
|
+
>
|
|
352
|
+
{s.name}
|
|
353
|
+
</li>
|
|
354
|
+
))}
|
|
355
|
+
</ul>
|
|
356
|
+
)}
|
|
357
|
+
</li>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
283
361
|
function TaskSection({ todos, live }: { todos: TodoItem[]; live: boolean }) {
|
|
284
362
|
const done = todos.filter((t) => t.status.toLowerCase() === "completed").length;
|
|
285
363
|
const active = todos.filter((t) => t.status.toLowerCase() !== "cancelled").length;
|
|
@@ -357,13 +435,13 @@ function ReferenceSection({ references }: { references: FileReference[] }) {
|
|
|
357
435
|
type="button"
|
|
358
436
|
onClick={() => setShowAll((v) => !v)}
|
|
359
437
|
aria-expanded={showAll}
|
|
360
|
-
className="mt-
|
|
438
|
+
className="mt-1.5 flex w-full items-center justify-center gap-1 rounded-lg py-1.5 text-[12px] text-muted-foreground outline-none transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40"
|
|
361
439
|
>
|
|
440
|
+
{showAll ? "Show less" : `Show ${hidden} more`}
|
|
362
441
|
<ChevronDownIcon
|
|
363
442
|
aria-hidden
|
|
364
443
|
className={cn("size-3 transition-transform duration-200", showAll && "rotate-180")}
|
|
365
444
|
/>
|
|
366
|
-
{showAll ? "Show less" : `Show ${hidden} more`}
|
|
367
445
|
</button>
|
|
368
446
|
)}
|
|
369
447
|
</>
|
|
@@ -432,9 +510,13 @@ function Section({
|
|
|
432
510
|
);
|
|
433
511
|
}
|
|
434
512
|
|
|
435
|
-
/**
|
|
513
|
+
/** Boxed list (libadwaita): kartu membulat, baris dipisah garis tipis. */
|
|
436
514
|
function Box({ children }: { children: ReactNode }) {
|
|
437
|
-
return
|
|
515
|
+
return (
|
|
516
|
+
<ul className="flex flex-col divide-y overflow-hidden rounded-xl border bg-card shadow-xs">
|
|
517
|
+
{children}
|
|
518
|
+
</ul>
|
|
519
|
+
);
|
|
438
520
|
}
|
|
439
521
|
|
|
440
522
|
/** Satu baris dalam `Box`: ikon + label (+ petunjuk kecil) + nilai di kanan. */
|
|
@@ -442,6 +524,7 @@ function Row({
|
|
|
442
524
|
icon,
|
|
443
525
|
label,
|
|
444
526
|
hint,
|
|
527
|
+
hintTone,
|
|
445
528
|
value,
|
|
446
529
|
muted,
|
|
447
530
|
mono,
|
|
@@ -450,13 +533,18 @@ function Row({
|
|
|
450
533
|
icon: ReactNode;
|
|
451
534
|
label: string;
|
|
452
535
|
hint?: string;
|
|
536
|
+
/** Warna baris kedua (mis. pesan error). */
|
|
537
|
+
hintTone?: "error";
|
|
453
538
|
value?: ReactNode;
|
|
454
539
|
muted?: boolean;
|
|
455
540
|
mono?: boolean;
|
|
456
541
|
title?: string;
|
|
457
542
|
}) {
|
|
458
543
|
return (
|
|
459
|
-
<li
|
|
544
|
+
<li
|
|
545
|
+
title={title}
|
|
546
|
+
className="flex min-h-9 min-w-0 items-center gap-2 px-2.5 py-1.5 text-[12.5px]"
|
|
547
|
+
>
|
|
460
548
|
<span
|
|
461
549
|
aria-hidden
|
|
462
550
|
className="flex shrink-0 items-center text-muted-foreground [&>svg]:size-3.5"
|
|
@@ -474,7 +562,14 @@ function Row({
|
|
|
474
562
|
{label}
|
|
475
563
|
</span>
|
|
476
564
|
{hint && (
|
|
477
|
-
<span
|
|
565
|
+
<span
|
|
566
|
+
className={cn(
|
|
567
|
+
"truncate text-[11px] leading-4",
|
|
568
|
+
hintTone === "error" ? "text-destructive/90" : "text-muted-foreground",
|
|
569
|
+
)}
|
|
570
|
+
>
|
|
571
|
+
{hint}
|
|
572
|
+
</span>
|
|
478
573
|
)}
|
|
479
574
|
</span>
|
|
480
575
|
{value !== undefined && <span className="shrink-0">{value}</span>}
|
|
@@ -508,7 +603,7 @@ function TodoRow({ todo }: { todo: TodoItem }) {
|
|
|
508
603
|
const running = status === "in_progress";
|
|
509
604
|
|
|
510
605
|
return (
|
|
511
|
-
<li className="flex min-w-0 items-start gap-2 px-
|
|
606
|
+
<li className="flex min-w-0 items-start gap-2 px-2.5 py-2 text-[12.5px] leading-snug">
|
|
512
607
|
{/* Ikon status dekoratif; statusnya dibacakan lewat teks sr-only di
|
|
513
608
|
bawah (aria-label pada span/svg biasa tidak dibaca pembaca layar). */}
|
|
514
609
|
<span aria-hidden className="mt-0.5 flex shrink-0 items-center">
|
|
@@ -537,8 +632,13 @@ function TodoRow({ todo }: { todo: TodoItem }) {
|
|
|
537
632
|
);
|
|
538
633
|
}
|
|
539
634
|
|
|
635
|
+
/** Keadaan kosong — tetap di dalam boxed list agar bentuk bagian konsisten. */
|
|
540
636
|
function Empty({ children }: { children: ReactNode }) {
|
|
541
|
-
return
|
|
637
|
+
return (
|
|
638
|
+
<p className="rounded-xl border border-dashed px-2.5 py-2.5 text-[12px] leading-snug text-muted-foreground">
|
|
639
|
+
{children}
|
|
640
|
+
</p>
|
|
641
|
+
);
|
|
542
642
|
}
|
|
543
643
|
|
|
544
644
|
function CardSkeleton() {
|