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,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel samping halaman Project — gaya Claude Projects: Instructions, MCP
|
|
3
|
+
* server, dan Skills dalam satu kolom di kanan daftar Session.
|
|
4
|
+
*
|
|
5
|
+
* Tiap seksi bisa dilipat (status diingat). Daftar berupa "grouped list"
|
|
6
|
+
* (kotak berdivider) dengan padding simetris — tanpa margin negatif.
|
|
7
|
+
* - MCP: `GET /api/projects/:id/mcp` — ikon, nama, pill status; error
|
|
8
|
+
* `failed` tampil inline. Yang bermasalah diurutkan paling atas.
|
|
9
|
+
* - Skills: `GET /api/projects/:id/skills` — pencarian (bila banyak) + chip
|
|
10
|
+
* asal (All / This project / Global / Built-in); deskripsi 1 baris, klik
|
|
11
|
+
* baris untuk membuka deskripsi lengkap.
|
|
12
|
+
* Tiap seksi memuat datanya sendiri sehingga seksi yang lambat tidak menahan
|
|
13
|
+
* seksi lain. Tombol reload memuat ulang keduanya.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
ChevronDownIcon,
|
|
17
|
+
PlugIcon,
|
|
18
|
+
RefreshCwIcon,
|
|
19
|
+
SearchIcon,
|
|
20
|
+
SparklesIcon,
|
|
21
|
+
XIcon,
|
|
22
|
+
} from "lucide-react";
|
|
23
|
+
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
|
24
|
+
import { PanelBody, PanelHint, PanelList, PanelSection } from "@/components/projects/PanelSection";
|
|
25
|
+
import { ProjectInstructions } from "@/components/projects/ProjectInstructions";
|
|
26
|
+
import { Button } from "@/components/ui/button";
|
|
27
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
28
|
+
import { ApiError, apiFetch } from "@/lib/api";
|
|
29
|
+
import {
|
|
30
|
+
describeMcpSummary,
|
|
31
|
+
filterSkills,
|
|
32
|
+
groupSkills,
|
|
33
|
+
MCP_STATUS_LABEL,
|
|
34
|
+
SKILL_SOURCE_LABEL,
|
|
35
|
+
} from "@/lib/project-extensions";
|
|
36
|
+
import { cn } from "@/lib/utils";
|
|
37
|
+
import type { McpServerInfo, SkillInfo, SkillSource } from "@/server/services/opencode-client";
|
|
38
|
+
import type { Project } from "@/types";
|
|
39
|
+
|
|
40
|
+
/** Skill ditampilkan terbatas dulu; sisanya lewat "Show more". */
|
|
41
|
+
const SKILL_PREVIEW = 5;
|
|
42
|
+
/** Kotak cari muncul hanya bila skill cukup banyak. */
|
|
43
|
+
const SKILL_SEARCH_THRESHOLD = 8;
|
|
44
|
+
|
|
45
|
+
type Load<T> =
|
|
46
|
+
| { phase: "loading" }
|
|
47
|
+
| { phase: "error"; message: string }
|
|
48
|
+
| { phase: "ready"; data: T };
|
|
49
|
+
|
|
50
|
+
/** Muat satu endpoint list; `reloadKey` berubah -> muat ulang. */
|
|
51
|
+
function useProjectList<T>(url: string, key: string, reloadKey: number): Load<T> {
|
|
52
|
+
const [state, setState] = useState<Load<T>>({ phase: "loading" });
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
// `reloadKey` hanya pemicu muat ulang.
|
|
55
|
+
void reloadKey;
|
|
56
|
+
let cancelled = false;
|
|
57
|
+
setState({ phase: "loading" });
|
|
58
|
+
apiFetch(url)
|
|
59
|
+
.then(async (res) => {
|
|
60
|
+
const body = (await res.json()) as Record<string, T>;
|
|
61
|
+
if (!cancelled) setState({ phase: "ready", data: body[key] as T });
|
|
62
|
+
})
|
|
63
|
+
.catch((e: unknown) => {
|
|
64
|
+
if (cancelled) return;
|
|
65
|
+
setState({ phase: "error", message: e instanceof ApiError ? e.message : "Failed to load" });
|
|
66
|
+
});
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
};
|
|
70
|
+
}, [url, key, reloadKey]);
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ProjectSidePanelProps {
|
|
75
|
+
project: Project;
|
|
76
|
+
onProjectChange: (project: Project) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function ProjectSidePanel({ project, onProjectChange }: ProjectSidePanelProps) {
|
|
80
|
+
const [reloadKey, setReloadKey] = useState(0);
|
|
81
|
+
const reload = useCallback(() => setReloadKey((k) => k + 1), []);
|
|
82
|
+
const mcp = useProjectList<McpServerInfo[]>(`/api/projects/${project.id}/mcp`, "mcp", reloadKey);
|
|
83
|
+
const skills = useProjectList<SkillInfo[]>(
|
|
84
|
+
`/api/projects/${project.id}/skills`,
|
|
85
|
+
"skills",
|
|
86
|
+
reloadKey,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex flex-col gap-3">
|
|
91
|
+
<ProjectInstructions project={project} onSaved={onProjectChange} />
|
|
92
|
+
<McpSection state={mcp} onReload={reload} />
|
|
93
|
+
<SkillsSection state={skills} />
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ListSkeleton({ rows = 3 }: { rows?: number }) {
|
|
99
|
+
return (
|
|
100
|
+
<PanelList>
|
|
101
|
+
{Array.from({ length: rows }, (_, i) => (
|
|
102
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: placeholder statis
|
|
103
|
+
<li key={i} className="flex items-center gap-3 px-3.5 py-3" aria-hidden>
|
|
104
|
+
<Skeleton className="size-7 shrink-0 rounded-md" />
|
|
105
|
+
<div className="flex flex-1 flex-col gap-1.5">
|
|
106
|
+
<Skeleton className="h-3.5" style={{ width: `${75 - i * 15}%` }} />
|
|
107
|
+
<Skeleton className="h-3 w-1/3" />
|
|
108
|
+
</div>
|
|
109
|
+
</li>
|
|
110
|
+
))}
|
|
111
|
+
</PanelList>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ErrorBox({ message, onRetry }: { message: string; onRetry?: () => void }) {
|
|
116
|
+
return (
|
|
117
|
+
<div className="flex items-center justify-between gap-3 bg-destructive/5 px-3.5 py-3">
|
|
118
|
+
<p className="min-w-0 text-xs leading-relaxed text-destructive">{message}</p>
|
|
119
|
+
{onRetry && (
|
|
120
|
+
<Button type="button" variant="outline" size="xs" onClick={onRetry} className="shrink-0">
|
|
121
|
+
Retry
|
|
122
|
+
</Button>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Ikon kotak kecil di awal baris daftar. */
|
|
129
|
+
function RowIcon({ children, className }: { children: ReactNode; className?: string }) {
|
|
130
|
+
return (
|
|
131
|
+
<span
|
|
132
|
+
className={cn(
|
|
133
|
+
"flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground [&_svg]:size-3.5",
|
|
134
|
+
className,
|
|
135
|
+
)}
|
|
136
|
+
aria-hidden
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</span>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------- MCP ----
|
|
144
|
+
|
|
145
|
+
/** Yang butuh perhatian di atas: failed -> needs_* -> connected -> sisanya. */
|
|
146
|
+
const MCP_ORDER: Record<McpServerInfo["status"], number> = {
|
|
147
|
+
failed: 0,
|
|
148
|
+
needs_auth: 1,
|
|
149
|
+
needs_client_registration: 1,
|
|
150
|
+
connected: 2,
|
|
151
|
+
unknown: 3,
|
|
152
|
+
disabled: 4,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const MCP_PILL: Record<McpServerInfo["status"], string> = {
|
|
156
|
+
connected: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
|
|
157
|
+
failed: "bg-destructive/10 text-destructive",
|
|
158
|
+
needs_auth: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
|
|
159
|
+
needs_client_registration: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
|
|
160
|
+
disabled: "bg-muted text-muted-foreground",
|
|
161
|
+
unknown: "bg-muted text-muted-foreground",
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function McpSection({ state, onReload }: { state: Load<McpServerInfo[]>; onReload: () => void }) {
|
|
165
|
+
const sorted = useMemo(
|
|
166
|
+
() =>
|
|
167
|
+
state.phase === "ready"
|
|
168
|
+
? [...state.data].sort(
|
|
169
|
+
(a, b) => MCP_ORDER[a.status] - MCP_ORDER[b.status] || a.name.localeCompare(b.name),
|
|
170
|
+
)
|
|
171
|
+
: [],
|
|
172
|
+
[state],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<PanelSection
|
|
177
|
+
id="mcp"
|
|
178
|
+
title="MCP servers"
|
|
179
|
+
icon={<PlugIcon />}
|
|
180
|
+
meta={state.phase === "ready" ? describeMcpSummary(state.data) : undefined}
|
|
181
|
+
action={
|
|
182
|
+
<Button
|
|
183
|
+
type="button"
|
|
184
|
+
variant="ghost"
|
|
185
|
+
size="icon-xs"
|
|
186
|
+
onClick={onReload}
|
|
187
|
+
disabled={state.phase === "loading"}
|
|
188
|
+
aria-label="Reload MCP servers and skills"
|
|
189
|
+
title="Reload"
|
|
190
|
+
className="size-8 text-muted-foreground"
|
|
191
|
+
>
|
|
192
|
+
<RefreshCwIcon className={cn(state.phase === "loading" && "animate-spin")} />
|
|
193
|
+
</Button>
|
|
194
|
+
}
|
|
195
|
+
>
|
|
196
|
+
{state.phase === "loading" ? (
|
|
197
|
+
<ListSkeleton rows={2} />
|
|
198
|
+
) : state.phase === "error" ? (
|
|
199
|
+
<ErrorBox message={state.message} onRetry={onReload} />
|
|
200
|
+
) : sorted.length === 0 ? (
|
|
201
|
+
<PanelHint>
|
|
202
|
+
No MCP servers yet. Add them under <code className="font-mono">mcp</code> in
|
|
203
|
+
opencode.json, then reload.
|
|
204
|
+
</PanelHint>
|
|
205
|
+
) : (
|
|
206
|
+
<PanelList>
|
|
207
|
+
{sorted.map((server) => (
|
|
208
|
+
<li key={server.name} className="flex items-start gap-3 px-3.5 py-3">
|
|
209
|
+
<RowIcon
|
|
210
|
+
className={cn(
|
|
211
|
+
server.status === "connected" &&
|
|
212
|
+
"bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
|
|
213
|
+
server.status === "failed" && "bg-destructive/10 text-destructive",
|
|
214
|
+
)}
|
|
215
|
+
>
|
|
216
|
+
<PlugIcon />
|
|
217
|
+
</RowIcon>
|
|
218
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1 pt-0.5">
|
|
219
|
+
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
220
|
+
<span className="truncate text-sm font-medium leading-5">{server.name}</span>
|
|
221
|
+
<span
|
|
222
|
+
className={cn(
|
|
223
|
+
"shrink-0 rounded-full px-2 py-0.5 text-[11px] font-medium leading-4",
|
|
224
|
+
MCP_PILL[server.status],
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
{MCP_STATUS_LABEL[server.status]}
|
|
228
|
+
</span>
|
|
229
|
+
</div>
|
|
230
|
+
{server.error && (
|
|
231
|
+
<p
|
|
232
|
+
className="line-clamp-2 font-mono text-[11px] leading-relaxed break-all text-destructive/90"
|
|
233
|
+
title={server.error}
|
|
234
|
+
>
|
|
235
|
+
{server.error}
|
|
236
|
+
</p>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</li>
|
|
240
|
+
))}
|
|
241
|
+
</PanelList>
|
|
242
|
+
)}
|
|
243
|
+
</PanelSection>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ------------------------------------------------------------- Skills ----
|
|
248
|
+
|
|
249
|
+
type SkillFilter = "all" | SkillSource;
|
|
250
|
+
|
|
251
|
+
function SkillsSection({ state }: { state: Load<SkillInfo[]> }) {
|
|
252
|
+
const [query, setQuery] = useState("");
|
|
253
|
+
const [filter, setFilter] = useState<SkillFilter>("all");
|
|
254
|
+
const [expanded, setExpanded] = useState(false);
|
|
255
|
+
|
|
256
|
+
const all = state.phase === "ready" ? state.data : [];
|
|
257
|
+
// Urutan tampil: Project -> global -> bawaan (paling relevan dulu).
|
|
258
|
+
const ordered = useMemo(() => groupSkills(all).flatMap((g) => g.skills), [all]);
|
|
259
|
+
const sources = useMemo(() => groupSkills(all).map((g) => g.source), [all]);
|
|
260
|
+
const bySource = useMemo(
|
|
261
|
+
() => (filter === "all" ? ordered : ordered.filter((s) => s.source === filter)),
|
|
262
|
+
[ordered, filter],
|
|
263
|
+
);
|
|
264
|
+
const filtered = useMemo(() => filterSkills(bySource, query), [bySource, query]);
|
|
265
|
+
const searching = query.trim() !== "";
|
|
266
|
+
// Saat mencari, tampilkan semua hasil; selain itu batasi hingga "Show more".
|
|
267
|
+
const visible = searching || expanded ? filtered : filtered.slice(0, SKILL_PREVIEW);
|
|
268
|
+
const hidden = filtered.length - visible.length;
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<PanelSection
|
|
272
|
+
id="skills"
|
|
273
|
+
title="Skills"
|
|
274
|
+
icon={<SparklesIcon />}
|
|
275
|
+
meta={
|
|
276
|
+
state.phase === "ready"
|
|
277
|
+
? all.length === 0
|
|
278
|
+
? "None found"
|
|
279
|
+
: `${all.length} available`
|
|
280
|
+
: undefined
|
|
281
|
+
}
|
|
282
|
+
>
|
|
283
|
+
{state.phase === "loading" ? (
|
|
284
|
+
<ListSkeleton rows={3} />
|
|
285
|
+
) : state.phase === "error" ? (
|
|
286
|
+
<ErrorBox message={state.message} />
|
|
287
|
+
) : all.length === 0 ? (
|
|
288
|
+
<PanelHint>
|
|
289
|
+
No skills found. Add a <code className="font-mono">SKILL.md</code> under{" "}
|
|
290
|
+
<code className="font-mono">.opencode/skills/</code>.
|
|
291
|
+
</PanelHint>
|
|
292
|
+
) : (
|
|
293
|
+
<div className="flex flex-col">
|
|
294
|
+
{(all.length >= SKILL_SEARCH_THRESHOLD || sources.length > 1) && (
|
|
295
|
+
<PanelBody className="flex flex-col gap-2.5 border-b">
|
|
296
|
+
{all.length >= SKILL_SEARCH_THRESHOLD && (
|
|
297
|
+
<div className="relative">
|
|
298
|
+
<SearchIcon
|
|
299
|
+
className="pointer-events-none absolute top-1/2 left-3 size-3.5 -translate-y-1/2 text-muted-foreground"
|
|
300
|
+
aria-hidden
|
|
301
|
+
/>
|
|
302
|
+
<input
|
|
303
|
+
type="search"
|
|
304
|
+
value={query}
|
|
305
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
306
|
+
onKeyDown={(e) => e.key === "Escape" && setQuery("")}
|
|
307
|
+
placeholder={`Search ${all.length} skills`}
|
|
308
|
+
aria-label="Search skills"
|
|
309
|
+
className="h-9 w-full rounded-lg border bg-background pr-8 pl-8.5 text-sm outline-none transition-shadow placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&::-webkit-search-cancel-button]:hidden"
|
|
310
|
+
/>
|
|
311
|
+
{query && (
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
onClick={() => setQuery("")}
|
|
315
|
+
aria-label="Clear search"
|
|
316
|
+
className="absolute top-1/2 right-2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:text-foreground"
|
|
317
|
+
>
|
|
318
|
+
<XIcon className="size-3.5" />
|
|
319
|
+
</button>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
|
|
324
|
+
{sources.length > 1 && (
|
|
325
|
+
<fieldset
|
|
326
|
+
aria-label="Filter skills by source"
|
|
327
|
+
className="m-0 flex flex-wrap gap-1 border-0 p-0"
|
|
328
|
+
>
|
|
329
|
+
{(["all", ...sources] as SkillFilter[]).map((f) => {
|
|
330
|
+
const count =
|
|
331
|
+
f === "all" ? all.length : all.filter((s) => s.source === f).length;
|
|
332
|
+
const active = filter === f;
|
|
333
|
+
return (
|
|
334
|
+
<button
|
|
335
|
+
key={f}
|
|
336
|
+
type="button"
|
|
337
|
+
aria-pressed={active}
|
|
338
|
+
onClick={() => {
|
|
339
|
+
setFilter(f);
|
|
340
|
+
setExpanded(false);
|
|
341
|
+
}}
|
|
342
|
+
className={cn(
|
|
343
|
+
"inline-flex h-7 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none",
|
|
344
|
+
active
|
|
345
|
+
? "bg-foreground text-background"
|
|
346
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
347
|
+
)}
|
|
348
|
+
>
|
|
349
|
+
{f === "all" ? "All" : SKILL_SOURCE_LABEL[f]}
|
|
350
|
+
<span className={cn("tabular-nums", active ? "opacity-70" : "opacity-60")}>
|
|
351
|
+
{count}
|
|
352
|
+
</span>
|
|
353
|
+
</button>
|
|
354
|
+
);
|
|
355
|
+
})}
|
|
356
|
+
</fieldset>
|
|
357
|
+
)}
|
|
358
|
+
</PanelBody>
|
|
359
|
+
)}
|
|
360
|
+
|
|
361
|
+
{visible.length === 0 ? (
|
|
362
|
+
<PanelHint>No skills match "{query.trim()}".</PanelHint>
|
|
363
|
+
) : (
|
|
364
|
+
<PanelList>
|
|
365
|
+
{visible.map((s) => (
|
|
366
|
+
<SkillRow key={`${s.source}:${s.name}`} skill={s} showSource={filter === "all"} />
|
|
367
|
+
))}
|
|
368
|
+
{!searching && filtered.length > SKILL_PREVIEW && (
|
|
369
|
+
<li>
|
|
370
|
+
<button
|
|
371
|
+
type="button"
|
|
372
|
+
onClick={() => setExpanded((v) => !v)}
|
|
373
|
+
aria-expanded={expanded}
|
|
374
|
+
className="flex w-full items-center justify-center gap-1.5 px-3.5 py-2.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:bg-muted/50 focus-visible:outline-none"
|
|
375
|
+
>
|
|
376
|
+
{expanded ? "Show less" : `Show ${hidden} more`}
|
|
377
|
+
<ChevronDownIcon
|
|
378
|
+
className={cn("size-3.5 transition-transform", expanded && "rotate-180")}
|
|
379
|
+
aria-hidden
|
|
380
|
+
/>
|
|
381
|
+
</button>
|
|
382
|
+
</li>
|
|
383
|
+
)}
|
|
384
|
+
</PanelList>
|
|
385
|
+
)}
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</PanelSection>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Satu skill: ikon, nama (+ label asal), deskripsi 1 baris. Klik baris untuk
|
|
394
|
+
* membuka deskripsi lengkap dan path SKILL.md.
|
|
395
|
+
*/
|
|
396
|
+
function SkillRow({ skill, showSource }: { skill: SkillInfo; showSource: boolean }) {
|
|
397
|
+
const [open, setOpen] = useState(false);
|
|
398
|
+
const expandable = Boolean(skill.description || skill.location);
|
|
399
|
+
return (
|
|
400
|
+
<li>
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
onClick={() => expandable && setOpen((v) => !v)}
|
|
404
|
+
aria-expanded={expandable ? open : undefined}
|
|
405
|
+
disabled={!expandable}
|
|
406
|
+
className="flex w-full items-start gap-3 px-3.5 py-3 text-left transition-colors hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:outline-none disabled:cursor-default disabled:hover:bg-transparent"
|
|
407
|
+
>
|
|
408
|
+
<RowIcon className={cn(skill.source === "project" && "bg-primary/10 text-primary")}>
|
|
409
|
+
<SparklesIcon />
|
|
410
|
+
</RowIcon>
|
|
411
|
+
<span className="flex min-w-0 flex-1 flex-col gap-0.5 pt-0.5">
|
|
412
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
413
|
+
<span className="truncate text-sm font-medium leading-5">{skill.name}</span>
|
|
414
|
+
{showSource && skill.source !== "global" && (
|
|
415
|
+
<span className="shrink-0 rounded bg-muted px-1.5 text-[10px] font-medium leading-4 text-muted-foreground">
|
|
416
|
+
{skill.source === "project" ? "Project" : "Built-in"}
|
|
417
|
+
</span>
|
|
418
|
+
)}
|
|
419
|
+
</span>
|
|
420
|
+
{skill.description && (
|
|
421
|
+
<span
|
|
422
|
+
className={cn(
|
|
423
|
+
"text-xs leading-relaxed text-muted-foreground",
|
|
424
|
+
open ? "whitespace-pre-line" : "line-clamp-1",
|
|
425
|
+
)}
|
|
426
|
+
>
|
|
427
|
+
{skill.description}
|
|
428
|
+
</span>
|
|
429
|
+
)}
|
|
430
|
+
{open && skill.location && (
|
|
431
|
+
<span className="mt-1 truncate font-mono text-[11px] text-muted-foreground/80">
|
|
432
|
+
{skill.location}
|
|
433
|
+
</span>
|
|
434
|
+
)}
|
|
435
|
+
</span>
|
|
436
|
+
{expandable && (
|
|
437
|
+
<ChevronDownIcon
|
|
438
|
+
className={cn(
|
|
439
|
+
"mt-1.5 size-3.5 shrink-0 text-muted-foreground/60 transition-transform",
|
|
440
|
+
open && "rotate-180",
|
|
441
|
+
)}
|
|
442
|
+
aria-hidden
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
445
|
+
</button>
|
|
446
|
+
</li>
|
|
447
|
+
);
|
|
448
|
+
}
|