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,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kartu konteks Session — keadaan kerja agent, MENGAPUNG di margin kanan.
|
|
3
|
+
* Tidak mengambil ruang layout, jadi percakapan & input tidak bergeser saat
|
|
4
|
+
* kartu dibuka/ditutup.
|
|
5
|
+
*
|
|
6
|
+
* Susunan (dari yang paling sering dilirik ke yang paling jarang):
|
|
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
|
+
* - **Task list** : `todowrite` agent + progres "selesai / total".
|
|
11
|
+
* - **Reference** : file project yang DIBACA agent (bukan yang ditulis),
|
|
12
|
+
* terbaru di atas; 8 pertama tampil, sisanya "Show N more".
|
|
13
|
+
*
|
|
14
|
+
* Pola UX:
|
|
15
|
+
* - Tiap bagian = judul yang bisa dilipat + ringkasan di kanan judul, jadi
|
|
16
|
+
* kartu tetap informatif walau semua bagian dilipat.
|
|
17
|
+
* - Tanpa judul kartu & tanpa kotak di dalam: judul tiap bagian (warna
|
|
18
|
+
* solid) menjadi penanda, dipisah garis tipis antar bagian.
|
|
19
|
+
* - Status diwakili titik berwarna + label, bukan warna saja (a11y).
|
|
20
|
+
* - Buka/tutup: `ContextPanelTrigger` (hamburger) HILANG saat kartu terbuka;
|
|
21
|
+
* penutupnya tombol X di kartu. Esc juga menutup.
|
|
22
|
+
* - Mobile: isi yang sama tampil sebagai bottom sheet (`ContextSheet`).
|
|
23
|
+
*
|
|
24
|
+
* Kartu ini hanya informasi (belum ada aksi git): barisnya tidak dibuat
|
|
25
|
+
* seperti tombol supaya tidak menyiratkan aksi yang belum ada.
|
|
26
|
+
*/
|
|
27
|
+
import {
|
|
28
|
+
CheckIcon,
|
|
29
|
+
ChevronDownIcon,
|
|
30
|
+
CircleIcon,
|
|
31
|
+
FileDiffIcon,
|
|
32
|
+
FileTextIcon,
|
|
33
|
+
GitBranchIcon,
|
|
34
|
+
LoaderIcon,
|
|
35
|
+
MenuIcon,
|
|
36
|
+
PlugIcon,
|
|
37
|
+
UploadIcon,
|
|
38
|
+
XIcon,
|
|
39
|
+
} from "lucide-react";
|
|
40
|
+
import * as m from "motion/react-m";
|
|
41
|
+
import { type ReactNode, useEffect, useState } from "react";
|
|
42
|
+
import { Button } from "@/components/ui/button";
|
|
43
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
44
|
+
import {
|
|
45
|
+
Sheet,
|
|
46
|
+
SheetContent,
|
|
47
|
+
SheetDescription,
|
|
48
|
+
SheetHeader,
|
|
49
|
+
SheetTitle,
|
|
50
|
+
} from "@/components/ui/sheet";
|
|
51
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
52
|
+
import { MCP_STATUS_DOT, MCP_STATUS_LABEL } from "@/lib/project-extensions";
|
|
53
|
+
import { baseName, type FileReference } from "@/lib/references";
|
|
54
|
+
import { cn } from "@/lib/utils";
|
|
55
|
+
import type { McpServerState, SessionContext, TodoItem } from "@/types";
|
|
56
|
+
|
|
57
|
+
// ----------------------------------------------------------- trigger ----
|
|
58
|
+
|
|
59
|
+
/** Tombol pembuka kartu — hanya dirender saat kartu tertutup. */
|
|
60
|
+
export function ContextPanelTrigger({ onOpen }: { onOpen: () => void }) {
|
|
61
|
+
return (
|
|
62
|
+
<Button
|
|
63
|
+
type="button"
|
|
64
|
+
variant="outline"
|
|
65
|
+
size="icon"
|
|
66
|
+
onClick={onOpen}
|
|
67
|
+
aria-label="Show session context"
|
|
68
|
+
title="Show context"
|
|
69
|
+
aria-expanded={false}
|
|
70
|
+
className="absolute end-3 top-2 z-30 size-9 rounded-full bg-background/80 shadow-sm backdrop-blur-sm sm:end-4"
|
|
71
|
+
>
|
|
72
|
+
<MenuIcon />
|
|
73
|
+
</Button>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// -------------------------------------------------------------- card ----
|
|
78
|
+
|
|
79
|
+
export interface ContextPanelProps {
|
|
80
|
+
context: SessionContext | null;
|
|
81
|
+
references: FileReference[];
|
|
82
|
+
loading: boolean;
|
|
83
|
+
onClose: () => void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Isi kartu (keempat bagian) — dipakai bersama oleh kartu desktop
|
|
88
|
+
* (`ContextPanel`) dan bottom sheet mobile (`ContextSheet`), jadi tampilan &
|
|
89
|
+
* perilakunya selalu identik.
|
|
90
|
+
*/
|
|
91
|
+
function ContextSections({
|
|
92
|
+
context,
|
|
93
|
+
references,
|
|
94
|
+
loading,
|
|
95
|
+
/** Cadangkan ruang tombol X di judul bagian pertama (kartu desktop). */
|
|
96
|
+
reserveClose = false,
|
|
97
|
+
}: Omit<ContextPanelProps, "onClose"> & { reserveClose?: boolean }) {
|
|
98
|
+
if (loading && context === null) return <CardSkeleton />;
|
|
99
|
+
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
|
+
>
|
|
106
|
+
<EnvironmentSection context={context} />
|
|
107
|
+
<McpSection servers={context?.mcp ?? []} live={context?.live ?? false} />
|
|
108
|
+
<TaskSection todos={context?.todos ?? []} live={context?.live ?? false} />
|
|
109
|
+
<ReferenceSection references={references} />
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Kartu desktop: mengapung di margin kanan. */
|
|
115
|
+
export function ContextPanel({ context, references, loading, onClose }: ContextPanelProps) {
|
|
116
|
+
// Esc menutup kartu (pola sama dengan popover/sheet aplikasi).
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const onKey = (e: KeyboardEvent) => {
|
|
119
|
+
if (e.key === "Escape") onClose();
|
|
120
|
+
};
|
|
121
|
+
window.addEventListener("keydown", onKey);
|
|
122
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
123
|
+
}, [onClose]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<m.aside
|
|
127
|
+
aria-label="Session context"
|
|
128
|
+
initial={{ opacity: 0, x: 10 }}
|
|
129
|
+
animate={{ opacity: 1, x: 0 }}
|
|
130
|
+
transition={{ type: "spring", visualDuration: 0.25, bounce: 0.1 }}
|
|
131
|
+
className="pointer-events-auto relative flex max-h-full flex-col overflow-hidden rounded-2xl border bg-popover shadow-lg"
|
|
132
|
+
>
|
|
133
|
+
{/* Tanpa judul kartu: judul tiap bagian sudah menjadi penanda. Tombol
|
|
134
|
+
tutup mengapung di pojok kanan atas, sejajar judul bagian pertama. */}
|
|
135
|
+
<Button
|
|
136
|
+
type="button"
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="icon"
|
|
139
|
+
onClick={onClose}
|
|
140
|
+
aria-label="Hide session context"
|
|
141
|
+
title="Hide context (Esc)"
|
|
142
|
+
className="absolute end-1.5 top-1.5 z-10 size-6 rounded-full [&_svg]:size-3.5"
|
|
143
|
+
>
|
|
144
|
+
<XIcon />
|
|
145
|
+
</Button>
|
|
146
|
+
|
|
147
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-2 pt-1.5 pb-2 [scrollbar-width:thin]">
|
|
148
|
+
<ContextSections context={context} references={references} loading={loading} reserveClose />
|
|
149
|
+
</div>
|
|
150
|
+
</m.aside>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Bottom sheet mobile: isi sama dengan kartu desktop. Sheet sudah menangani
|
|
156
|
+
* overlay, drag handle, Esc, tap di luar, dan focus trap — jadi tidak perlu
|
|
157
|
+
* tombol X (pola sama dengan sheet aksi composer).
|
|
158
|
+
*/
|
|
159
|
+
export function ContextSheet({
|
|
160
|
+
open,
|
|
161
|
+
onOpenChange,
|
|
162
|
+
context,
|
|
163
|
+
references,
|
|
164
|
+
loading,
|
|
165
|
+
}: Omit<ContextPanelProps, "onClose"> & {
|
|
166
|
+
open: boolean;
|
|
167
|
+
onOpenChange: (open: boolean) => void;
|
|
168
|
+
}) {
|
|
169
|
+
return (
|
|
170
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
171
|
+
<SheetContent
|
|
172
|
+
side="bottom"
|
|
173
|
+
className="max-h-[80dvh] gap-0 pb-[max(0.75rem,env(safe-area-inset-bottom))]"
|
|
174
|
+
>
|
|
175
|
+
<SheetHeader className="sr-only">
|
|
176
|
+
<SheetTitle>Session context</SheetTitle>
|
|
177
|
+
<SheetDescription>
|
|
178
|
+
Environment, MCP servers, task list, and files read by the agent.
|
|
179
|
+
</SheetDescription>
|
|
180
|
+
</SheetHeader>
|
|
181
|
+
<div className="min-h-0 overflow-y-auto overscroll-contain px-3 pt-2 pb-1">
|
|
182
|
+
<ContextSections context={context} references={references} loading={loading} />
|
|
183
|
+
</div>
|
|
184
|
+
</SheetContent>
|
|
185
|
+
</Sheet>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ----------------------------------------------------------- sections ----
|
|
190
|
+
|
|
191
|
+
function EnvironmentSection({ context }: { context: SessionContext | null }) {
|
|
192
|
+
const changed = context?.changes ?? [];
|
|
193
|
+
const added = changed.reduce((n, c) => n + c.added, 0);
|
|
194
|
+
const removed = changed.reduce((n, c) => n + c.removed, 0);
|
|
195
|
+
const git = context?.git;
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Section title="Environment">
|
|
199
|
+
<Box>
|
|
200
|
+
<Row
|
|
201
|
+
icon={<FileDiffIcon />}
|
|
202
|
+
label={
|
|
203
|
+
changed.length === 0
|
|
204
|
+
? "No changes"
|
|
205
|
+
: `${changed.length} ${changed.length === 1 ? "file" : "files"} changed`
|
|
206
|
+
}
|
|
207
|
+
value={changed.length > 0 ? <DiffStat added={added} removed={removed} /> : undefined}
|
|
208
|
+
/>
|
|
209
|
+
<Row
|
|
210
|
+
icon={<GitBranchIcon />}
|
|
211
|
+
label={git?.branch ?? "Not a git repository"}
|
|
212
|
+
muted={!git?.branch}
|
|
213
|
+
mono={Boolean(git?.branch)}
|
|
214
|
+
/>
|
|
215
|
+
{git?.branch && (
|
|
216
|
+
<Row
|
|
217
|
+
icon={<UploadIcon />}
|
|
218
|
+
label={syncLabel(git.ahead, git.behind, git.hasRemote)}
|
|
219
|
+
muted
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</Box>
|
|
223
|
+
</Section>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Urutan tampil MCP: yang bermasalah dulu agar langsung terlihat. */
|
|
228
|
+
const MCP_ORDER: Record<McpServerState["status"], number> = {
|
|
229
|
+
failed: 0,
|
|
230
|
+
needs_auth: 1,
|
|
231
|
+
needs_client_registration: 2,
|
|
232
|
+
connected: 3,
|
|
233
|
+
unknown: 4,
|
|
234
|
+
disabled: 5,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
function McpSection({ servers, live }: { servers: McpServerState[]; live: boolean }) {
|
|
238
|
+
const connected = servers.filter((s) => s.status === "connected").length;
|
|
239
|
+
const hasProblem = servers.some((s) => MCP_ORDER[s.status] <= 2);
|
|
240
|
+
const sorted = [...servers].sort(
|
|
241
|
+
(a, b) => MCP_ORDER[a.status] - MCP_ORDER[b.status] || a.name.localeCompare(b.name),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<Section
|
|
246
|
+
title="MCP"
|
|
247
|
+
summary={
|
|
248
|
+
servers.length > 0 ? (
|
|
249
|
+
<span className={cn(hasProblem && "text-amber-700 dark:text-amber-400")}>
|
|
250
|
+
{connected}/{servers.length} connected
|
|
251
|
+
</span>
|
|
252
|
+
) : undefined
|
|
253
|
+
}
|
|
254
|
+
>
|
|
255
|
+
{servers.length === 0 ? (
|
|
256
|
+
<Empty>{live ? "No MCP servers configured" : "Start the session to see MCP servers"}</Empty>
|
|
257
|
+
) : (
|
|
258
|
+
<Box>
|
|
259
|
+
{sorted.map((server) => (
|
|
260
|
+
<Row
|
|
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
|
+
/>
|
|
276
|
+
))}
|
|
277
|
+
</Box>
|
|
278
|
+
)}
|
|
279
|
+
</Section>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function TaskSection({ todos, live }: { todos: TodoItem[]; live: boolean }) {
|
|
284
|
+
const done = todos.filter((t) => t.status.toLowerCase() === "completed").length;
|
|
285
|
+
const active = todos.filter((t) => t.status.toLowerCase() !== "cancelled").length;
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<Section title="Task list" summary={todos.length > 0 ? `${done}/${active} done` : undefined}>
|
|
289
|
+
{todos.length === 0 ? (
|
|
290
|
+
<Empty>{live ? "The agent hasn't planned any tasks yet" : "Session is not running"}</Empty>
|
|
291
|
+
) : (
|
|
292
|
+
<div className="flex flex-col gap-2">
|
|
293
|
+
{/* Progres: bar tipis — cepat dibaca tanpa menghitung item. */}
|
|
294
|
+
{active > 0 && (
|
|
295
|
+
<div
|
|
296
|
+
role="progressbar"
|
|
297
|
+
aria-label="Task progress"
|
|
298
|
+
aria-valuemin={0}
|
|
299
|
+
aria-valuemax={active}
|
|
300
|
+
aria-valuenow={done}
|
|
301
|
+
className="mx-1 h-1 overflow-hidden rounded-full bg-muted"
|
|
302
|
+
>
|
|
303
|
+
<div
|
|
304
|
+
className="h-full rounded-full bg-emerald-500 transition-[width] duration-500"
|
|
305
|
+
style={{ width: `${(done / active) * 100}%` }}
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
<Box>
|
|
310
|
+
{todos.map((t, i) => (
|
|
311
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: urutan = urutan task list agent
|
|
312
|
+
<TodoRow key={`${i}-${t.content}`} todo={t} />
|
|
313
|
+
))}
|
|
314
|
+
</Box>
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</Section>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Jumlah file yang langsung tampil. Sisanya di balik "Show N more" supaya
|
|
323
|
+
* Reference tidak mendorong bagian lain (Task list, MCP) keluar pandangan
|
|
324
|
+
* saat agent membaca puluhan file. Daftar sudah terbaru-di-atas, jadi yang
|
|
325
|
+
* tersembunyi adalah file yang paling lama tidak disentuh.
|
|
326
|
+
*/
|
|
327
|
+
export const REFERENCE_PREVIEW = 8;
|
|
328
|
+
|
|
329
|
+
function ReferenceSection({ references }: { references: FileReference[] }) {
|
|
330
|
+
const [showAll, setShowAll] = useState(false);
|
|
331
|
+
const hidden = Math.max(0, references.length - REFERENCE_PREVIEW);
|
|
332
|
+
const visible = showAll ? references : references.slice(0, REFERENCE_PREVIEW);
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<Section
|
|
336
|
+
title="Reference"
|
|
337
|
+
summary={references.length > 0 ? String(references.length) : undefined}
|
|
338
|
+
>
|
|
339
|
+
{references.length === 0 ? (
|
|
340
|
+
<Empty>Files the agent reads will appear here</Empty>
|
|
341
|
+
) : (
|
|
342
|
+
<>
|
|
343
|
+
<Box>
|
|
344
|
+
{visible.map((ref) => (
|
|
345
|
+
<Row
|
|
346
|
+
key={ref.path}
|
|
347
|
+
icon={<FileTextIcon />}
|
|
348
|
+
label={baseName(ref.path)}
|
|
349
|
+
// Folder induk sebagai konteks; path lengkap di `title`.
|
|
350
|
+
hint={parentDir(ref.path)}
|
|
351
|
+
title={ref.path}
|
|
352
|
+
/>
|
|
353
|
+
))}
|
|
354
|
+
</Box>
|
|
355
|
+
{hidden > 0 && (
|
|
356
|
+
<button
|
|
357
|
+
type="button"
|
|
358
|
+
onClick={() => setShowAll((v) => !v)}
|
|
359
|
+
aria-expanded={showAll}
|
|
360
|
+
className="mt-0.5 flex w-full items-center gap-1 rounded-md px-1 py-1 text-left text-[12px] text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40"
|
|
361
|
+
>
|
|
362
|
+
<ChevronDownIcon
|
|
363
|
+
aria-hidden
|
|
364
|
+
className={cn("size-3 transition-transform duration-200", showAll && "rotate-180")}
|
|
365
|
+
/>
|
|
366
|
+
{showAll ? "Show less" : `Show ${hidden} more`}
|
|
367
|
+
</button>
|
|
368
|
+
)}
|
|
369
|
+
</>
|
|
370
|
+
)}
|
|
371
|
+
</Section>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ------------------------------------------------------------ helpers ----
|
|
376
|
+
|
|
377
|
+
/** Teks sinkronisasi cabang terhadap upstream. */
|
|
378
|
+
function syncLabel(ahead: number | null, behind: number | null, hasRemote: boolean): string {
|
|
379
|
+
if (!hasRemote) return "No remote";
|
|
380
|
+
if (ahead === null) return "Not pushed yet";
|
|
381
|
+
const pull = behind ?? 0;
|
|
382
|
+
if (ahead === 0 && pull === 0) return "Up to date";
|
|
383
|
+
const parts: string[] = [];
|
|
384
|
+
if (ahead > 0) parts.push(`${ahead} to push`);
|
|
385
|
+
if (pull > 0) parts.push(`${pull} to pull`);
|
|
386
|
+
return parts.join(" · ");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Folder induk dari path relatif ("src/lib/a.ts" -> "src/lib"). */
|
|
390
|
+
function parentDir(p: string): string | undefined {
|
|
391
|
+
const i = p.lastIndexOf("/");
|
|
392
|
+
return i > 0 ? p.slice(0, i) : undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------- primitives ----
|
|
396
|
+
|
|
397
|
+
/** Bagian yang bisa dilipat: judul + ringkasan di kanan judul. */
|
|
398
|
+
function Section({
|
|
399
|
+
title,
|
|
400
|
+
summary,
|
|
401
|
+
children,
|
|
402
|
+
}: {
|
|
403
|
+
title: string;
|
|
404
|
+
summary?: ReactNode;
|
|
405
|
+
children: ReactNode;
|
|
406
|
+
}) {
|
|
407
|
+
const [expanded, setExpanded] = useState(true);
|
|
408
|
+
return (
|
|
409
|
+
<Collapsible open={expanded} onOpenChange={setExpanded} className="flex flex-col gap-1.5">
|
|
410
|
+
<CollapsibleTrigger asChild>
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
className="group/section flex min-h-7 items-center gap-1 rounded-md px-1 text-left outline-none hover:bg-muted/50 focus-visible:ring-[3px] focus-visible:ring-ring/40"
|
|
414
|
+
>
|
|
415
|
+
<ChevronDownIcon
|
|
416
|
+
aria-hidden
|
|
417
|
+
className={cn(
|
|
418
|
+
"size-3 shrink-0 text-muted-foreground transition-transform duration-200",
|
|
419
|
+
!expanded && "-rotate-90",
|
|
420
|
+
)}
|
|
421
|
+
/>
|
|
422
|
+
<span className="text-[13px] font-semibold tracking-tight text-foreground">{title}</span>
|
|
423
|
+
{summary !== undefined && (
|
|
424
|
+
<span className="ml-auto truncate ps-2 text-[11px] text-muted-foreground">
|
|
425
|
+
{summary}
|
|
426
|
+
</span>
|
|
427
|
+
)}
|
|
428
|
+
</button>
|
|
429
|
+
</CollapsibleTrigger>
|
|
430
|
+
<CollapsibleContent>{children}</CollapsibleContent>
|
|
431
|
+
</Collapsible>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Daftar baris polos (tanpa kotak/garis — hemat ruang). */
|
|
436
|
+
function Box({ children }: { children: ReactNode }) {
|
|
437
|
+
return <ul className="flex flex-col">{children}</ul>;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Satu baris dalam `Box`: ikon + label (+ petunjuk kecil) + nilai di kanan. */
|
|
441
|
+
function Row({
|
|
442
|
+
icon,
|
|
443
|
+
label,
|
|
444
|
+
hint,
|
|
445
|
+
value,
|
|
446
|
+
muted,
|
|
447
|
+
mono,
|
|
448
|
+
title,
|
|
449
|
+
}: {
|
|
450
|
+
icon: ReactNode;
|
|
451
|
+
label: string;
|
|
452
|
+
hint?: string;
|
|
453
|
+
value?: ReactNode;
|
|
454
|
+
muted?: boolean;
|
|
455
|
+
mono?: boolean;
|
|
456
|
+
title?: string;
|
|
457
|
+
}) {
|
|
458
|
+
return (
|
|
459
|
+
<li title={title} className="flex min-h-8 min-w-0 items-center gap-2 px-1 py-1 text-[12.5px]">
|
|
460
|
+
<span
|
|
461
|
+
aria-hidden
|
|
462
|
+
className="flex shrink-0 items-center text-muted-foreground [&>svg]:size-3.5"
|
|
463
|
+
>
|
|
464
|
+
{icon}
|
|
465
|
+
</span>
|
|
466
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
467
|
+
<span
|
|
468
|
+
className={cn(
|
|
469
|
+
"truncate leading-5",
|
|
470
|
+
muted && "text-muted-foreground",
|
|
471
|
+
mono && "font-mono text-[12px]",
|
|
472
|
+
)}
|
|
473
|
+
>
|
|
474
|
+
{label}
|
|
475
|
+
</span>
|
|
476
|
+
{hint && (
|
|
477
|
+
<span className="truncate text-[11px] leading-4 text-muted-foreground">{hint}</span>
|
|
478
|
+
)}
|
|
479
|
+
</span>
|
|
480
|
+
{value !== undefined && <span className="shrink-0">{value}</span>}
|
|
481
|
+
</li>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** +tambah / -hapus berwarna. */
|
|
486
|
+
function DiffStat({ added, removed }: { added: number; removed: number }) {
|
|
487
|
+
return (
|
|
488
|
+
<span className="flex items-center gap-1.5 font-mono text-[11.5px] tabular-nums">
|
|
489
|
+
<span className="text-emerald-600 dark:text-emerald-400">+{added}</span>
|
|
490
|
+
<span className="text-destructive">-{removed}</span>
|
|
491
|
+
</span>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Label status task untuk pembaca layar. */
|
|
496
|
+
const TODO_STATUS_LABEL: Record<string, string> = {
|
|
497
|
+
completed: "Completed",
|
|
498
|
+
in_progress: "In progress",
|
|
499
|
+
pending: "Pending",
|
|
500
|
+
cancelled: "Cancelled",
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
/** Satu item task list: ikon status + teks (selesai dicoret). */
|
|
504
|
+
function TodoRow({ todo }: { todo: TodoItem }) {
|
|
505
|
+
const status = todo.status.toLowerCase();
|
|
506
|
+
const done = status === "completed";
|
|
507
|
+
const cancelled = status === "cancelled";
|
|
508
|
+
const running = status === "in_progress";
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<li className="flex min-w-0 items-start gap-2 px-1 py-1 text-[12.5px] leading-snug">
|
|
512
|
+
{/* Ikon status dekoratif; statusnya dibacakan lewat teks sr-only di
|
|
513
|
+
bawah (aria-label pada span/svg biasa tidak dibaca pembaca layar). */}
|
|
514
|
+
<span aria-hidden className="mt-0.5 flex shrink-0 items-center">
|
|
515
|
+
{done ? (
|
|
516
|
+
<span className="flex size-3.5 items-center justify-center rounded-full bg-emerald-500 text-white">
|
|
517
|
+
<CheckIcon className="size-2" strokeWidth={4} />
|
|
518
|
+
</span>
|
|
519
|
+
) : running ? (
|
|
520
|
+
<LoaderIcon className="size-3.5 animate-spin text-sky-600 motion-reduce:animate-none dark:text-sky-400" />
|
|
521
|
+
) : (
|
|
522
|
+
<CircleIcon className="size-3.5 text-muted-foreground/60" />
|
|
523
|
+
)}
|
|
524
|
+
</span>
|
|
525
|
+
<span className="sr-only">{TODO_STATUS_LABEL[status] ?? "Pending"}: </span>
|
|
526
|
+
<span
|
|
527
|
+
className={cn(
|
|
528
|
+
"min-w-0 break-words",
|
|
529
|
+
done && "text-muted-foreground line-through decoration-muted-foreground/50",
|
|
530
|
+
cancelled && "text-muted-foreground/60 line-through",
|
|
531
|
+
running && "font-medium",
|
|
532
|
+
)}
|
|
533
|
+
>
|
|
534
|
+
{todo.content}
|
|
535
|
+
</span>
|
|
536
|
+
</li>
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function Empty({ children }: { children: ReactNode }) {
|
|
541
|
+
return <p className="px-1 py-0.5 text-[12px] leading-snug text-muted-foreground">{children}</p>;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function CardSkeleton() {
|
|
545
|
+
return (
|
|
546
|
+
<div className="flex flex-col gap-3" aria-hidden>
|
|
547
|
+
{[3, 2, 2].map((rows, i) => (
|
|
548
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: kerangka statis
|
|
549
|
+
<div key={i} className="flex flex-col gap-1.5">
|
|
550
|
+
<Skeleton className="mx-1 h-3.5 w-24" />
|
|
551
|
+
<div className="flex flex-col gap-1.5 px-1">
|
|
552
|
+
{Array.from({ length: rows }, (_, j) => (
|
|
553
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: kerangka statis
|
|
554
|
+
<Skeleton key={j} className="h-6" />
|
|
555
|
+
))}
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
))}
|
|
559
|
+
</div>
|
|
560
|
+
);
|
|
561
|
+
}
|