kcg-code 0.0.0-stage → 0.2.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 +230 -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 +84 -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__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -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 +147 -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 +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -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 +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -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 +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -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 +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -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 +256 -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 +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -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 +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -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/search-entry.tsx +70 -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 +114 -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 +101 -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 +327 -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 +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -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 +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -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 +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -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 +362 -0
- package/src/lib/skills-catalog.ts +138 -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 +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -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 +427 -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 +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -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__/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__/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 +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -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/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/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 +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -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,661 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*
|
|
6
|
+
* Susunan (dari yang paling sering dilirik ke yang paling jarang):
|
|
7
|
+
* - **Environment** : perubahan file (+/-), cabang git, status push/pull.
|
|
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.
|
|
14
|
+
* - **Reference** : file project yang DIBACA agent (bukan yang ditulis),
|
|
15
|
+
* terbaru di atas; 8 pertama tampil, sisanya "Show N more".
|
|
16
|
+
*
|
|
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.
|
|
21
|
+
* - Status diwakili titik berwarna + label, bukan warna saja (a11y).
|
|
22
|
+
* - Buka/tutup: `ContextPanelTrigger` (hamburger) HILANG saat kartu terbuka;
|
|
23
|
+
* penutupnya tombol X di kartu. Esc juga menutup.
|
|
24
|
+
* - Mobile: isi yang sama tampil sebagai bottom sheet (`ContextSheet`).
|
|
25
|
+
*
|
|
26
|
+
* Kartu ini hanya informasi (belum ada aksi git): barisnya tidak dibuat
|
|
27
|
+
* seperti tombol supaya tidak menyiratkan aksi yang belum ada.
|
|
28
|
+
*/
|
|
29
|
+
import {
|
|
30
|
+
CheckIcon,
|
|
31
|
+
ChevronDownIcon,
|
|
32
|
+
CircleIcon,
|
|
33
|
+
FileDiffIcon,
|
|
34
|
+
FileTextIcon,
|
|
35
|
+
GitBranchIcon,
|
|
36
|
+
LoaderIcon,
|
|
37
|
+
MenuIcon,
|
|
38
|
+
PlugIcon,
|
|
39
|
+
UploadIcon,
|
|
40
|
+
XIcon,
|
|
41
|
+
} from "lucide-react";
|
|
42
|
+
import * as m from "motion/react-m";
|
|
43
|
+
import { type ReactNode, useEffect, useState } from "react";
|
|
44
|
+
import { Button } from "@/components/ui/button";
|
|
45
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
46
|
+
import {
|
|
47
|
+
Sheet,
|
|
48
|
+
SheetContent,
|
|
49
|
+
SheetDescription,
|
|
50
|
+
SheetHeader,
|
|
51
|
+
SheetTitle,
|
|
52
|
+
} from "@/components/ui/sheet";
|
|
53
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
54
|
+
import { MCP_STATUS_DOT, MCP_STATUS_LABEL } from "@/lib/project-extensions";
|
|
55
|
+
import { baseName, type FileReference } from "@/lib/references";
|
|
56
|
+
import { cn } from "@/lib/utils";
|
|
57
|
+
import type { McpServerState, SessionContext, TodoItem } from "@/types";
|
|
58
|
+
|
|
59
|
+
// ----------------------------------------------------------- trigger ----
|
|
60
|
+
|
|
61
|
+
/** Tombol pembuka kartu — hanya dirender saat kartu tertutup. */
|
|
62
|
+
export function ContextPanelTrigger({ onOpen }: { onOpen: () => void }) {
|
|
63
|
+
return (
|
|
64
|
+
<Button
|
|
65
|
+
type="button"
|
|
66
|
+
variant="outline"
|
|
67
|
+
size="icon"
|
|
68
|
+
onClick={onOpen}
|
|
69
|
+
aria-label="Show session context"
|
|
70
|
+
title="Show context"
|
|
71
|
+
aria-expanded={false}
|
|
72
|
+
className="absolute end-3 top-2 z-30 size-9 rounded-full bg-background/80 shadow-sm backdrop-blur-sm sm:end-4"
|
|
73
|
+
>
|
|
74
|
+
<MenuIcon />
|
|
75
|
+
</Button>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// -------------------------------------------------------------- card ----
|
|
80
|
+
|
|
81
|
+
export interface ContextPanelProps {
|
|
82
|
+
context: SessionContext | null;
|
|
83
|
+
references: FileReference[];
|
|
84
|
+
loading: boolean;
|
|
85
|
+
onClose: () => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Isi kartu (keempat bagian) — dipakai bersama oleh kartu desktop
|
|
90
|
+
* (`ContextPanel`) dan bottom sheet mobile (`ContextSheet`), jadi tampilan &
|
|
91
|
+
* perilakunya selalu identik.
|
|
92
|
+
*/
|
|
93
|
+
function ContextSections({
|
|
94
|
+
context,
|
|
95
|
+
references,
|
|
96
|
+
loading,
|
|
97
|
+
/** Cadangkan ruang tombol X di judul bagian pertama (kartu desktop). */
|
|
98
|
+
reserveClose = false,
|
|
99
|
+
}: Omit<ContextPanelProps, "onClose"> & { reserveClose?: boolean }) {
|
|
100
|
+
if (loading && context === null) return <CardSkeleton />;
|
|
101
|
+
return (
|
|
102
|
+
<div className={cn("flex flex-col gap-4", reserveClose && "[&>*:first-child>button]:pe-7")}>
|
|
103
|
+
<EnvironmentSection context={context} />
|
|
104
|
+
<TaskSection todos={context?.todos ?? []} live={context?.live ?? false} />
|
|
105
|
+
<McpSection servers={context?.mcp ?? []} live={context?.live ?? false} />
|
|
106
|
+
<ReferenceSection references={references} />
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Kartu desktop: kolom kanan, berdampingan dengan percakapan. */
|
|
112
|
+
export function ContextPanel({ context, references, loading, onClose }: ContextPanelProps) {
|
|
113
|
+
// Esc menutup kartu (pola sama dengan popover/sheet aplikasi).
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const onKey = (e: KeyboardEvent) => {
|
|
116
|
+
if (e.key === "Escape") onClose();
|
|
117
|
+
};
|
|
118
|
+
window.addEventListener("keydown", onKey);
|
|
119
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
120
|
+
}, [onClose]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<m.aside
|
|
124
|
+
aria-label="Session context"
|
|
125
|
+
initial={{ opacity: 0, x: 10 }}
|
|
126
|
+
animate={{ opacity: 1, x: 0 }}
|
|
127
|
+
transition={{ type: "spring", visualDuration: 0.25, bounce: 0.1 }}
|
|
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"
|
|
131
|
+
>
|
|
132
|
+
{/* Tanpa judul kartu: judul tiap bagian sudah menjadi penanda. Tombol
|
|
133
|
+
tutup mengapung di pojok kanan atas, sejajar judul bagian pertama. */}
|
|
134
|
+
<Button
|
|
135
|
+
type="button"
|
|
136
|
+
variant="ghost"
|
|
137
|
+
size="icon"
|
|
138
|
+
onClick={onClose}
|
|
139
|
+
aria-label="Hide session context"
|
|
140
|
+
title="Hide context (Esc)"
|
|
141
|
+
className="absolute end-1.5 top-1.5 z-10 size-6 rounded-full [&_svg]:size-3.5"
|
|
142
|
+
>
|
|
143
|
+
<XIcon />
|
|
144
|
+
</Button>
|
|
145
|
+
|
|
146
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-2.5 pt-2 pb-3 [scrollbar-width:thin]">
|
|
147
|
+
<ContextSections context={context} references={references} loading={loading} reserveClose />
|
|
148
|
+
</div>
|
|
149
|
+
</m.aside>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Bottom sheet mobile: isi sama dengan kartu desktop. Sheet sudah menangani
|
|
155
|
+
* overlay, drag handle, Esc, tap di luar, dan focus trap — jadi tidak perlu
|
|
156
|
+
* tombol X (pola sama dengan sheet aksi composer).
|
|
157
|
+
*/
|
|
158
|
+
export function ContextSheet({
|
|
159
|
+
open,
|
|
160
|
+
onOpenChange,
|
|
161
|
+
context,
|
|
162
|
+
references,
|
|
163
|
+
loading,
|
|
164
|
+
}: Omit<ContextPanelProps, "onClose"> & {
|
|
165
|
+
open: boolean;
|
|
166
|
+
onOpenChange: (open: boolean) => void;
|
|
167
|
+
}) {
|
|
168
|
+
return (
|
|
169
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
170
|
+
<SheetContent
|
|
171
|
+
side="bottom"
|
|
172
|
+
className="max-h-[80dvh] gap-0 pb-[max(0.75rem,env(safe-area-inset-bottom))]"
|
|
173
|
+
>
|
|
174
|
+
<SheetHeader className="sr-only">
|
|
175
|
+
<SheetTitle>Session context</SheetTitle>
|
|
176
|
+
<SheetDescription>
|
|
177
|
+
Environment, MCP servers, task list, and files read by the agent.
|
|
178
|
+
</SheetDescription>
|
|
179
|
+
</SheetHeader>
|
|
180
|
+
<div className="min-h-0 overflow-y-auto overscroll-contain px-3 pt-2 pb-1">
|
|
181
|
+
<ContextSections context={context} references={references} loading={loading} />
|
|
182
|
+
</div>
|
|
183
|
+
</SheetContent>
|
|
184
|
+
</Sheet>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ----------------------------------------------------------- sections ----
|
|
189
|
+
|
|
190
|
+
function EnvironmentSection({ context }: { context: SessionContext | null }) {
|
|
191
|
+
const changed = context?.changes ?? [];
|
|
192
|
+
const added = changed.reduce((n, c) => n + c.added, 0);
|
|
193
|
+
const removed = changed.reduce((n, c) => n + c.removed, 0);
|
|
194
|
+
const git = context?.git;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<Section title="Environment">
|
|
198
|
+
<Box>
|
|
199
|
+
<Row
|
|
200
|
+
icon={<FileDiffIcon />}
|
|
201
|
+
label={
|
|
202
|
+
changed.length === 0
|
|
203
|
+
? "No changes"
|
|
204
|
+
: `${changed.length} ${changed.length === 1 ? "file" : "files"} changed`
|
|
205
|
+
}
|
|
206
|
+
value={changed.length > 0 ? <DiffStat added={added} removed={removed} /> : undefined}
|
|
207
|
+
/>
|
|
208
|
+
<Row
|
|
209
|
+
icon={<GitBranchIcon />}
|
|
210
|
+
label={git?.branch ?? "Not a git repository"}
|
|
211
|
+
muted={!git?.branch}
|
|
212
|
+
mono={Boolean(git?.branch)}
|
|
213
|
+
/>
|
|
214
|
+
{git?.branch && (
|
|
215
|
+
<Row
|
|
216
|
+
icon={<UploadIcon />}
|
|
217
|
+
label={syncLabel(git.ahead, git.behind, git.hasRemote)}
|
|
218
|
+
muted
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
</Box>
|
|
222
|
+
</Section>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
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"];
|
|
228
|
+
|
|
229
|
+
function McpSection({ servers, live }: { servers: McpServerState[]; live: boolean }) {
|
|
230
|
+
const connected = servers.filter((s) => s.status === "connected").length;
|
|
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
|
+
];
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<Section
|
|
248
|
+
title="MCP"
|
|
249
|
+
summary={
|
|
250
|
+
servers.length > 0 ? (
|
|
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")}>
|
|
254
|
+
{connected}/{servers.length} connected
|
|
255
|
+
</span>
|
|
256
|
+
) : undefined
|
|
257
|
+
}
|
|
258
|
+
>
|
|
259
|
+
{servers.length === 0 ? (
|
|
260
|
+
<Empty>{live ? "No MCP servers configured" : "Start the session to see MCP servers"}</Empty>
|
|
261
|
+
) : (
|
|
262
|
+
<Box>
|
|
263
|
+
{individual.map((server) => (
|
|
264
|
+
<McpRow key={server.name} server={server} />
|
|
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
|
+
)}
|
|
280
|
+
</Box>
|
|
281
|
+
)}
|
|
282
|
+
</Section>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
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
|
+
|
|
361
|
+
function TaskSection({ todos, live }: { todos: TodoItem[]; live: boolean }) {
|
|
362
|
+
const done = todos.filter((t) => t.status.toLowerCase() === "completed").length;
|
|
363
|
+
const active = todos.filter((t) => t.status.toLowerCase() !== "cancelled").length;
|
|
364
|
+
|
|
365
|
+
return (
|
|
366
|
+
<Section title="Task list" summary={todos.length > 0 ? `${done}/${active} done` : undefined}>
|
|
367
|
+
{todos.length === 0 ? (
|
|
368
|
+
<Empty>{live ? "The agent hasn't planned any tasks yet" : "Session is not running"}</Empty>
|
|
369
|
+
) : (
|
|
370
|
+
<div className="flex flex-col gap-2">
|
|
371
|
+
{/* Progres: bar tipis — cepat dibaca tanpa menghitung item. */}
|
|
372
|
+
{active > 0 && (
|
|
373
|
+
<div
|
|
374
|
+
role="progressbar"
|
|
375
|
+
aria-label="Task progress"
|
|
376
|
+
aria-valuemin={0}
|
|
377
|
+
aria-valuemax={active}
|
|
378
|
+
aria-valuenow={done}
|
|
379
|
+
className="mx-1 h-1 overflow-hidden rounded-full bg-muted"
|
|
380
|
+
>
|
|
381
|
+
<div
|
|
382
|
+
className="h-full rounded-full bg-emerald-500 transition-[width] duration-500"
|
|
383
|
+
style={{ width: `${(done / active) * 100}%` }}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
<Box>
|
|
388
|
+
{todos.map((t, i) => (
|
|
389
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: urutan = urutan task list agent
|
|
390
|
+
<TodoRow key={`${i}-${t.content}`} todo={t} />
|
|
391
|
+
))}
|
|
392
|
+
</Box>
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
</Section>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Jumlah file yang langsung tampil. Sisanya di balik "Show N more" supaya
|
|
401
|
+
* Reference tidak mendorong bagian lain (Task list, MCP) keluar pandangan
|
|
402
|
+
* saat agent membaca puluhan file. Daftar sudah terbaru-di-atas, jadi yang
|
|
403
|
+
* tersembunyi adalah file yang paling lama tidak disentuh.
|
|
404
|
+
*/
|
|
405
|
+
export const REFERENCE_PREVIEW = 8;
|
|
406
|
+
|
|
407
|
+
function ReferenceSection({ references }: { references: FileReference[] }) {
|
|
408
|
+
const [showAll, setShowAll] = useState(false);
|
|
409
|
+
const hidden = Math.max(0, references.length - REFERENCE_PREVIEW);
|
|
410
|
+
const visible = showAll ? references : references.slice(0, REFERENCE_PREVIEW);
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<Section
|
|
414
|
+
title="Reference"
|
|
415
|
+
summary={references.length > 0 ? String(references.length) : undefined}
|
|
416
|
+
>
|
|
417
|
+
{references.length === 0 ? (
|
|
418
|
+
<Empty>Files the agent reads will appear here</Empty>
|
|
419
|
+
) : (
|
|
420
|
+
<>
|
|
421
|
+
<Box>
|
|
422
|
+
{visible.map((ref) => (
|
|
423
|
+
<Row
|
|
424
|
+
key={ref.path}
|
|
425
|
+
icon={<FileTextIcon />}
|
|
426
|
+
label={baseName(ref.path)}
|
|
427
|
+
// Folder induk sebagai konteks; path lengkap di `title`.
|
|
428
|
+
hint={parentDir(ref.path)}
|
|
429
|
+
title={ref.path}
|
|
430
|
+
/>
|
|
431
|
+
))}
|
|
432
|
+
</Box>
|
|
433
|
+
{hidden > 0 && (
|
|
434
|
+
<button
|
|
435
|
+
type="button"
|
|
436
|
+
onClick={() => setShowAll((v) => !v)}
|
|
437
|
+
aria-expanded={showAll}
|
|
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"
|
|
439
|
+
>
|
|
440
|
+
{showAll ? "Show less" : `Show ${hidden} more`}
|
|
441
|
+
<ChevronDownIcon
|
|
442
|
+
aria-hidden
|
|
443
|
+
className={cn("size-3 transition-transform duration-200", showAll && "rotate-180")}
|
|
444
|
+
/>
|
|
445
|
+
</button>
|
|
446
|
+
)}
|
|
447
|
+
</>
|
|
448
|
+
)}
|
|
449
|
+
</Section>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ------------------------------------------------------------ helpers ----
|
|
454
|
+
|
|
455
|
+
/** Teks sinkronisasi cabang terhadap upstream. */
|
|
456
|
+
function syncLabel(ahead: number | null, behind: number | null, hasRemote: boolean): string {
|
|
457
|
+
if (!hasRemote) return "No remote";
|
|
458
|
+
if (ahead === null) return "Not pushed yet";
|
|
459
|
+
const pull = behind ?? 0;
|
|
460
|
+
if (ahead === 0 && pull === 0) return "Up to date";
|
|
461
|
+
const parts: string[] = [];
|
|
462
|
+
if (ahead > 0) parts.push(`${ahead} to push`);
|
|
463
|
+
if (pull > 0) parts.push(`${pull} to pull`);
|
|
464
|
+
return parts.join(" · ");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Folder induk dari path relatif ("src/lib/a.ts" -> "src/lib"). */
|
|
468
|
+
function parentDir(p: string): string | undefined {
|
|
469
|
+
const i = p.lastIndexOf("/");
|
|
470
|
+
return i > 0 ? p.slice(0, i) : undefined;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ---------------------------------------------------------- primitives ----
|
|
474
|
+
|
|
475
|
+
/** Bagian yang bisa dilipat: judul + ringkasan di kanan judul. */
|
|
476
|
+
function Section({
|
|
477
|
+
title,
|
|
478
|
+
summary,
|
|
479
|
+
children,
|
|
480
|
+
}: {
|
|
481
|
+
title: string;
|
|
482
|
+
summary?: ReactNode;
|
|
483
|
+
children: ReactNode;
|
|
484
|
+
}) {
|
|
485
|
+
const [expanded, setExpanded] = useState(true);
|
|
486
|
+
return (
|
|
487
|
+
<Collapsible open={expanded} onOpenChange={setExpanded} className="flex flex-col gap-1.5">
|
|
488
|
+
<CollapsibleTrigger asChild>
|
|
489
|
+
<button
|
|
490
|
+
type="button"
|
|
491
|
+
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"
|
|
492
|
+
>
|
|
493
|
+
<ChevronDownIcon
|
|
494
|
+
aria-hidden
|
|
495
|
+
className={cn(
|
|
496
|
+
"size-3 shrink-0 text-muted-foreground transition-transform duration-200",
|
|
497
|
+
!expanded && "-rotate-90",
|
|
498
|
+
)}
|
|
499
|
+
/>
|
|
500
|
+
<span className="text-[13px] font-semibold tracking-tight text-foreground">{title}</span>
|
|
501
|
+
{summary !== undefined && (
|
|
502
|
+
<span className="ml-auto truncate ps-2 text-[11px] text-muted-foreground">
|
|
503
|
+
{summary}
|
|
504
|
+
</span>
|
|
505
|
+
)}
|
|
506
|
+
</button>
|
|
507
|
+
</CollapsibleTrigger>
|
|
508
|
+
<CollapsibleContent>{children}</CollapsibleContent>
|
|
509
|
+
</Collapsible>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Boxed list (libadwaita): kartu membulat, baris dipisah garis tipis. */
|
|
514
|
+
function Box({ children }: { children: ReactNode }) {
|
|
515
|
+
return (
|
|
516
|
+
<ul className="flex flex-col divide-y overflow-hidden rounded-xl border bg-card shadow-xs">
|
|
517
|
+
{children}
|
|
518
|
+
</ul>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Satu baris dalam `Box`: ikon + label (+ petunjuk kecil) + nilai di kanan. */
|
|
523
|
+
function Row({
|
|
524
|
+
icon,
|
|
525
|
+
label,
|
|
526
|
+
hint,
|
|
527
|
+
hintTone,
|
|
528
|
+
value,
|
|
529
|
+
muted,
|
|
530
|
+
mono,
|
|
531
|
+
title,
|
|
532
|
+
}: {
|
|
533
|
+
icon: ReactNode;
|
|
534
|
+
label: string;
|
|
535
|
+
hint?: string;
|
|
536
|
+
/** Warna baris kedua (mis. pesan error). */
|
|
537
|
+
hintTone?: "error";
|
|
538
|
+
value?: ReactNode;
|
|
539
|
+
muted?: boolean;
|
|
540
|
+
mono?: boolean;
|
|
541
|
+
title?: string;
|
|
542
|
+
}) {
|
|
543
|
+
return (
|
|
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
|
+
>
|
|
548
|
+
<span
|
|
549
|
+
aria-hidden
|
|
550
|
+
className="flex shrink-0 items-center text-muted-foreground [&>svg]:size-3.5"
|
|
551
|
+
>
|
|
552
|
+
{icon}
|
|
553
|
+
</span>
|
|
554
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
555
|
+
<span
|
|
556
|
+
className={cn(
|
|
557
|
+
"truncate leading-5",
|
|
558
|
+
muted && "text-muted-foreground",
|
|
559
|
+
mono && "font-mono text-[12px]",
|
|
560
|
+
)}
|
|
561
|
+
>
|
|
562
|
+
{label}
|
|
563
|
+
</span>
|
|
564
|
+
{hint && (
|
|
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>
|
|
573
|
+
)}
|
|
574
|
+
</span>
|
|
575
|
+
{value !== undefined && <span className="shrink-0">{value}</span>}
|
|
576
|
+
</li>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** +tambah / -hapus berwarna. */
|
|
581
|
+
function DiffStat({ added, removed }: { added: number; removed: number }) {
|
|
582
|
+
return (
|
|
583
|
+
<span className="flex items-center gap-1.5 font-mono text-[11.5px] tabular-nums">
|
|
584
|
+
<span className="text-emerald-600 dark:text-emerald-400">+{added}</span>
|
|
585
|
+
<span className="text-destructive">-{removed}</span>
|
|
586
|
+
</span>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Label status task untuk pembaca layar. */
|
|
591
|
+
const TODO_STATUS_LABEL: Record<string, string> = {
|
|
592
|
+
completed: "Completed",
|
|
593
|
+
in_progress: "In progress",
|
|
594
|
+
pending: "Pending",
|
|
595
|
+
cancelled: "Cancelled",
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
/** Satu item task list: ikon status + teks (selesai dicoret). */
|
|
599
|
+
function TodoRow({ todo }: { todo: TodoItem }) {
|
|
600
|
+
const status = todo.status.toLowerCase();
|
|
601
|
+
const done = status === "completed";
|
|
602
|
+
const cancelled = status === "cancelled";
|
|
603
|
+
const running = status === "in_progress";
|
|
604
|
+
|
|
605
|
+
return (
|
|
606
|
+
<li className="flex min-w-0 items-start gap-2 px-2.5 py-2 text-[12.5px] leading-snug">
|
|
607
|
+
{/* Ikon status dekoratif; statusnya dibacakan lewat teks sr-only di
|
|
608
|
+
bawah (aria-label pada span/svg biasa tidak dibaca pembaca layar). */}
|
|
609
|
+
<span aria-hidden className="mt-0.5 flex shrink-0 items-center">
|
|
610
|
+
{done ? (
|
|
611
|
+
<span className="flex size-3.5 items-center justify-center rounded-full bg-emerald-500 text-white">
|
|
612
|
+
<CheckIcon className="size-2" strokeWidth={4} />
|
|
613
|
+
</span>
|
|
614
|
+
) : running ? (
|
|
615
|
+
<LoaderIcon className="size-3.5 animate-spin text-sky-600 motion-reduce:animate-none dark:text-sky-400" />
|
|
616
|
+
) : (
|
|
617
|
+
<CircleIcon className="size-3.5 text-muted-foreground/60" />
|
|
618
|
+
)}
|
|
619
|
+
</span>
|
|
620
|
+
<span className="sr-only">{TODO_STATUS_LABEL[status] ?? "Pending"}: </span>
|
|
621
|
+
<span
|
|
622
|
+
className={cn(
|
|
623
|
+
"min-w-0 break-words",
|
|
624
|
+
done && "text-muted-foreground line-through decoration-muted-foreground/50",
|
|
625
|
+
cancelled && "text-muted-foreground/60 line-through",
|
|
626
|
+
running && "font-medium",
|
|
627
|
+
)}
|
|
628
|
+
>
|
|
629
|
+
{todo.content}
|
|
630
|
+
</span>
|
|
631
|
+
</li>
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Keadaan kosong — tetap di dalam boxed list agar bentuk bagian konsisten. */
|
|
636
|
+
function Empty({ children }: { children: ReactNode }) {
|
|
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
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function CardSkeleton() {
|
|
645
|
+
return (
|
|
646
|
+
<div className="flex flex-col gap-3" aria-hidden>
|
|
647
|
+
{[3, 2, 2].map((rows, i) => (
|
|
648
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: kerangka statis
|
|
649
|
+
<div key={i} className="flex flex-col gap-1.5">
|
|
650
|
+
<Skeleton className="mx-1 h-3.5 w-24" />
|
|
651
|
+
<div className="flex flex-col gap-1.5 px-1">
|
|
652
|
+
{Array.from({ length: rows }, (_, j) => (
|
|
653
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: kerangka statis
|
|
654
|
+
<Skeleton key={j} className="h-6" />
|
|
655
|
+
))}
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
))}
|
|
659
|
+
</div>
|
|
660
|
+
);
|
|
661
|
+
}
|