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,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar aplikasi: seluruh Session digroup per Project (shadcn `sidebar`).
|
|
3
|
+
*
|
|
4
|
+
* - Desktop (>= md): panel tetap di kiri, bisa diciutkan (tombol trigger /
|
|
5
|
+
* Ctrl+B). Mobile: Sheet off-canvas yang otomatis tertutup setelah memilih
|
|
6
|
+
* Session agar layar penuh kembali untuk chat.
|
|
7
|
+
* - Tiap Project = Collapsible berisi daftar Session (running di atas).
|
|
8
|
+
* Klik sekali = expand/collapse; dobel klik (mouse) = buka halaman Project.
|
|
9
|
+
* Layar sentuh & keyboard tidak punya dobel klik, jadi baris terakhir tiap
|
|
10
|
+
* Project selalu berupa tautan ke halaman Project ("Show all (n)" bila ada
|
|
11
|
+
* Session tersembunyi, selain itu "Project details").
|
|
12
|
+
* - Navigasi utama di atas daftar: "New session" (homepage/composer chat),
|
|
13
|
+
* "Search" (command palette `SearchDialog`, juga Ctrl/⌘+K), dan "Skills"
|
|
14
|
+
* (`/skills`).
|
|
15
|
+
* - Desktop: hover baris Session memunculkan menu "..." (Start/Stop/
|
|
16
|
+
* Restart, Delete) — `SidebarSessionMenu`. HP tidak (tanpa hover).
|
|
17
|
+
* - Footer: pemilih tema (Light / Dark / System), lalu profil pemilik +
|
|
18
|
+
* tombol Settings & Lock (`SidebarAccount`).
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
ChevronRightIcon,
|
|
22
|
+
FolderIcon,
|
|
23
|
+
FolderOpenIcon,
|
|
24
|
+
MessageCirclePlusIcon,
|
|
25
|
+
RefreshCwIcon,
|
|
26
|
+
SearchIcon,
|
|
27
|
+
SparklesIcon,
|
|
28
|
+
} from "lucide-react";
|
|
29
|
+
import type * as React from "react";
|
|
30
|
+
import { useEffect, useRef, useState } from "react";
|
|
31
|
+
import { SearchDialog } from "@/components/layout/SearchDialog";
|
|
32
|
+
import { SidebarAccount } from "@/components/layout/SidebarAccount";
|
|
33
|
+
import { SidebarSessionMenu } from "@/components/layout/SidebarSessionMenu";
|
|
34
|
+
import { ThemeToggle } from "@/components/layout/ThemeToggle";
|
|
35
|
+
import { Button } from "@/components/ui/button";
|
|
36
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
37
|
+
import {
|
|
38
|
+
Sidebar,
|
|
39
|
+
SidebarContent,
|
|
40
|
+
SidebarFooter,
|
|
41
|
+
SidebarGroup,
|
|
42
|
+
SidebarGroupContent,
|
|
43
|
+
SidebarGroupLabel,
|
|
44
|
+
SidebarHeader,
|
|
45
|
+
SidebarMenu,
|
|
46
|
+
SidebarMenuBadge,
|
|
47
|
+
SidebarMenuButton,
|
|
48
|
+
SidebarMenuItem,
|
|
49
|
+
SidebarMenuSkeleton,
|
|
50
|
+
SidebarMenuSub,
|
|
51
|
+
SidebarMenuSubButton,
|
|
52
|
+
SidebarMenuSubItem,
|
|
53
|
+
SidebarRail,
|
|
54
|
+
useSidebar,
|
|
55
|
+
} from "@/components/ui/sidebar";
|
|
56
|
+
import { useRouter } from "@/hooks/useRouter";
|
|
57
|
+
import type { UseSidebarDataResult } from "@/hooks/useSidebarData";
|
|
58
|
+
import { type AppRoute, projectPath, projectsPath, sessionPath, skillsPath } from "@/lib/routes";
|
|
59
|
+
import { SESSION_STATUS_DOT, SESSION_STATUS_LABEL } from "@/lib/session-status";
|
|
60
|
+
import { displaySessionTitle } from "@/lib/session-title";
|
|
61
|
+
import type { SidebarProjectGroup } from "@/lib/sidebar-groups";
|
|
62
|
+
import { cn } from "@/lib/utils";
|
|
63
|
+
|
|
64
|
+
import logo from "../../logo.svg";
|
|
65
|
+
|
|
66
|
+
/** Indentasi kiri tombol navigasi utama (New session / Search / Skills). */
|
|
67
|
+
const NAV_BUTTON_CLASS = "pl-4";
|
|
68
|
+
|
|
69
|
+
/** Batas Session per Project yang tampil sebelum tautan "Show all". */
|
|
70
|
+
const MAX_VISIBLE_SESSIONS = 5;
|
|
71
|
+
|
|
72
|
+
export interface AppSidebarProps {
|
|
73
|
+
route: AppRoute;
|
|
74
|
+
data: UseSidebarDataResult;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function AppSidebar({ route, data }: AppSidebarProps) {
|
|
78
|
+
const { navigate } = useRouter();
|
|
79
|
+
const { isMobile, setOpenMobile } = useSidebar();
|
|
80
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
81
|
+
|
|
82
|
+
// Pintasan global Ctrl/⌘+K membuka pencarian.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const onKey = (e: KeyboardEvent) => {
|
|
85
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
setSearchOpen((o) => !o);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener("keydown", onKey);
|
|
91
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
/** Navigasi SPA dari tautan sidebar; Sheet mobile ditutup setelahnya. */
|
|
95
|
+
const go = (path: string) => (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
96
|
+
// Biarkan ctrl/cmd/middle-click membuka tab baru seperti tautan biasa.
|
|
97
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
navigate(path);
|
|
100
|
+
if (isMobile) setOpenMobile(false);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const activeProjectId =
|
|
104
|
+
route.name === "project" || route.name === "session" ? route.projectId : null;
|
|
105
|
+
const activeSessionId = route.name === "session" ? route.sessionId : null;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Sidebar collapsible="offcanvas">
|
|
109
|
+
<SidebarHeader>
|
|
110
|
+
<SidebarMenu>
|
|
111
|
+
<SidebarMenuItem>
|
|
112
|
+
<SidebarMenuButton size="lg" asChild>
|
|
113
|
+
<a href={projectsPath()} onClick={go(projectsPath())}>
|
|
114
|
+
{/* Tint warna logo (sama dengan Settings -> About) agar logo
|
|
115
|
+
ungu tetap kontras di light & dark mode. */}
|
|
116
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-[#EEEBFF] p-1.5 ring-1 ring-[#6D5CFB]/20 dark:bg-[#6D5CFB]/15 dark:ring-[#6D5CFB]/30">
|
|
117
|
+
<img src={logo} alt="" className="size-full" />
|
|
118
|
+
</span>
|
|
119
|
+
<span className="truncate font-semibold">KCG Code</span>
|
|
120
|
+
</a>
|
|
121
|
+
</SidebarMenuButton>
|
|
122
|
+
</SidebarMenuItem>
|
|
123
|
+
</SidebarMenu>
|
|
124
|
+
|
|
125
|
+
{/* Navigasi utama */}
|
|
126
|
+
<SidebarMenu className="mt-1">
|
|
127
|
+
<SidebarMenuItem>
|
|
128
|
+
{/* Homepage = composer chat: kirim pesan di sana membuat Session
|
|
129
|
+
baru di Project yang dipilih. */}
|
|
130
|
+
<SidebarMenuButton
|
|
131
|
+
asChild
|
|
132
|
+
isActive={route.name === "projects"}
|
|
133
|
+
className={NAV_BUTTON_CLASS}
|
|
134
|
+
>
|
|
135
|
+
<a href={projectsPath()} onClick={go(projectsPath())}>
|
|
136
|
+
<MessageCirclePlusIcon />
|
|
137
|
+
<span>New session</span>
|
|
138
|
+
</a>
|
|
139
|
+
</SidebarMenuButton>
|
|
140
|
+
</SidebarMenuItem>
|
|
141
|
+
<SidebarMenuItem>
|
|
142
|
+
<SidebarMenuButton
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => {
|
|
145
|
+
if (isMobile) setOpenMobile(false);
|
|
146
|
+
setSearchOpen(true);
|
|
147
|
+
}}
|
|
148
|
+
aria-haspopup="dialog"
|
|
149
|
+
aria-keyshortcuts="Control+K Meta+K"
|
|
150
|
+
className={NAV_BUTTON_CLASS}
|
|
151
|
+
>
|
|
152
|
+
<SearchIcon />
|
|
153
|
+
<span>Search</span>
|
|
154
|
+
<kbd className="ml-auto hidden rounded border bg-sidebar px-1.5 font-mono text-[10px] text-muted-foreground md:inline">
|
|
155
|
+
Ctrl K
|
|
156
|
+
</kbd>
|
|
157
|
+
</SidebarMenuButton>
|
|
158
|
+
</SidebarMenuItem>
|
|
159
|
+
<SidebarMenuItem>
|
|
160
|
+
<SidebarMenuButton
|
|
161
|
+
asChild
|
|
162
|
+
isActive={route.name === "skills"}
|
|
163
|
+
className={NAV_BUTTON_CLASS}
|
|
164
|
+
>
|
|
165
|
+
{/* Bawa Project aktif agar otomatis terpilih di halaman Skills. */}
|
|
166
|
+
<a href={skillsPath(activeProjectId)} onClick={go(skillsPath(activeProjectId))}>
|
|
167
|
+
<SparklesIcon />
|
|
168
|
+
<span>Skills</span>
|
|
169
|
+
</a>
|
|
170
|
+
</SidebarMenuButton>
|
|
171
|
+
</SidebarMenuItem>
|
|
172
|
+
</SidebarMenu>
|
|
173
|
+
</SidebarHeader>
|
|
174
|
+
|
|
175
|
+
<SidebarContent>
|
|
176
|
+
<SidebarGroup>
|
|
177
|
+
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
|
178
|
+
<SidebarGroupContent>
|
|
179
|
+
<SidebarBody
|
|
180
|
+
data={data}
|
|
181
|
+
activeProjectId={activeProjectId}
|
|
182
|
+
activeSessionId={activeSessionId}
|
|
183
|
+
go={go}
|
|
184
|
+
navigate={(path) => {
|
|
185
|
+
navigate(path);
|
|
186
|
+
if (isMobile) setOpenMobile(false);
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
</SidebarGroupContent>
|
|
190
|
+
</SidebarGroup>
|
|
191
|
+
</SidebarContent>
|
|
192
|
+
|
|
193
|
+
<SidebarFooter className="gap-3 border-t">
|
|
194
|
+
{/* Tema: Light / Dark / System */}
|
|
195
|
+
<ThemeToggle />
|
|
196
|
+
{/* Profil + Settings + Lock */}
|
|
197
|
+
<SidebarAccount
|
|
198
|
+
active={route.name === "settings"}
|
|
199
|
+
onNavigate={(path) => {
|
|
200
|
+
navigate(path);
|
|
201
|
+
if (isMobile) setOpenMobile(false);
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
</SidebarFooter>
|
|
205
|
+
|
|
206
|
+
<SidebarRail />
|
|
207
|
+
|
|
208
|
+
<SearchDialog
|
|
209
|
+
open={searchOpen}
|
|
210
|
+
onOpenChange={setSearchOpen}
|
|
211
|
+
groups={data.groups}
|
|
212
|
+
onSelect={navigate}
|
|
213
|
+
/>
|
|
214
|
+
</Sidebar>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type GoHandler = (path: string) => (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
219
|
+
/** Navigasi langsung (tanpa event anchor) — dipakai dobel klik Project. */
|
|
220
|
+
type NavigateFn = (path: string) => void;
|
|
221
|
+
|
|
222
|
+
interface SidebarBodyProps {
|
|
223
|
+
data: UseSidebarDataResult;
|
|
224
|
+
activeProjectId: string | null;
|
|
225
|
+
activeSessionId: string | null;
|
|
226
|
+
go: GoHandler;
|
|
227
|
+
navigate: NavigateFn;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Isi grup: skeleton, error, kosong, atau daftar Project. */
|
|
231
|
+
function SidebarBody({ data, activeProjectId, activeSessionId, go, navigate }: SidebarBodyProps) {
|
|
232
|
+
if (data.initialLoading) {
|
|
233
|
+
return (
|
|
234
|
+
<SidebarMenu aria-busy="true" aria-label="Loading sessions">
|
|
235
|
+
{["a", "b", "c", "d"].map((k) => (
|
|
236
|
+
<SidebarMenuItem key={k}>
|
|
237
|
+
<SidebarMenuSkeleton showIcon />
|
|
238
|
+
</SidebarMenuItem>
|
|
239
|
+
))}
|
|
240
|
+
</SidebarMenu>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (data.loadError && data.projectCount === 0) {
|
|
245
|
+
return (
|
|
246
|
+
<div className="flex flex-col items-start gap-2 px-2 py-1 text-xs text-muted-foreground">
|
|
247
|
+
<p role="alert">{data.loadError}</p>
|
|
248
|
+
<Button type="button" variant="outline" size="sm" onClick={() => void data.refresh()}>
|
|
249
|
+
<RefreshCwIcon data-icon="inline-start" />
|
|
250
|
+
Retry
|
|
251
|
+
</Button>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (data.projectCount === 0) {
|
|
257
|
+
return <p className="px-2 py-1 text-xs text-muted-foreground">No projects yet.</p>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<SidebarMenu>
|
|
262
|
+
{data.groups.map((group) => (
|
|
263
|
+
<ProjectGroup
|
|
264
|
+
key={group.project.id}
|
|
265
|
+
group={group}
|
|
266
|
+
active={group.project.id === activeProjectId}
|
|
267
|
+
activeSessionId={activeSessionId}
|
|
268
|
+
go={go}
|
|
269
|
+
navigate={navigate}
|
|
270
|
+
/>
|
|
271
|
+
))}
|
|
272
|
+
</SidebarMenu>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface ProjectGroupProps {
|
|
277
|
+
group: SidebarProjectGroup;
|
|
278
|
+
/** Project sedang dibuka (halaman Project atau salah satu Session-nya). */
|
|
279
|
+
active: boolean;
|
|
280
|
+
activeSessionId: string | null;
|
|
281
|
+
go: GoHandler;
|
|
282
|
+
navigate: NavigateFn;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function ProjectGroup({ group, active, activeSessionId, go, navigate }: ProjectGroupProps) {
|
|
286
|
+
const { isMobile } = useSidebar();
|
|
287
|
+
const [open, setOpen] = useState(active || group.sessions.length > 0);
|
|
288
|
+
/**
|
|
289
|
+
* Jenis pointer klik terakhir. Dobel klik hanya berlaku untuk mouse —
|
|
290
|
+
* double-tap di layar sentuh bisa ikut menghasilkan `detail === 2` dan
|
|
291
|
+
* tidak boleh tiba-tiba berpindah halaman.
|
|
292
|
+
*/
|
|
293
|
+
const pointerType = useRef<string>("mouse");
|
|
294
|
+
const { project, sessions, runningCount } = group;
|
|
295
|
+
const visible = sessions.slice(0, MAX_VISIBLE_SESSIONS);
|
|
296
|
+
// Session aktif di luar potongan tetap ditampilkan agar posisi terlihat.
|
|
297
|
+
const activeHidden =
|
|
298
|
+
activeSessionId !== null &&
|
|
299
|
+
!visible.some((s) => s.id === activeSessionId) &&
|
|
300
|
+
sessions.find((s) => s.id === activeSessionId);
|
|
301
|
+
const shown = activeHidden ? [...visible, activeHidden] : visible;
|
|
302
|
+
const hiddenCount = sessions.length - shown.length;
|
|
303
|
+
const detailPath = projectPath(project.id);
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Klik pertama (detail 1) dibiarkan ke Collapsible = toggle. Klik kedua
|
|
307
|
+
* dari dobel klik mouse (detail 2) membatalkan toggle-nya
|
|
308
|
+
* (`preventDefault` dihormati Radix) lalu membuka halaman Project; grup
|
|
309
|
+
* dipastikan terbuka agar Session-nya tetap terlihat.
|
|
310
|
+
*/
|
|
311
|
+
const onProjectClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
312
|
+
if (e.detail >= 2 && pointerType.current === "mouse") {
|
|
313
|
+
e.preventDefault();
|
|
314
|
+
setOpen(true);
|
|
315
|
+
navigate(detailPath);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<Collapsible asChild open={open} onOpenChange={setOpen} className="group/collapsible">
|
|
321
|
+
<SidebarMenuItem>
|
|
322
|
+
<CollapsibleTrigger asChild>
|
|
323
|
+
<SidebarMenuButton
|
|
324
|
+
isActive={active && activeSessionId === null}
|
|
325
|
+
tooltip={project.name}
|
|
326
|
+
aria-label={`${project.name}, ${sessions.length} sessions`}
|
|
327
|
+
onPointerDown={(e) => {
|
|
328
|
+
pointerType.current = e.pointerType;
|
|
329
|
+
}}
|
|
330
|
+
onClick={onProjectClick}
|
|
331
|
+
// Petunjuk dobel klik hanya untuk pointer mouse (bukan HP).
|
|
332
|
+
title={isMobile ? undefined : "Click to expand · Double-click to open project"}
|
|
333
|
+
// Dobel klik tidak boleh ikut memblok (select) nama Project.
|
|
334
|
+
className="select-none"
|
|
335
|
+
>
|
|
336
|
+
<ChevronRightIcon className="transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
337
|
+
{active ? <FolderOpenIcon /> : <FolderIcon />}
|
|
338
|
+
<span>{project.name}</span>
|
|
339
|
+
</SidebarMenuButton>
|
|
340
|
+
</CollapsibleTrigger>
|
|
341
|
+
{runningCount > 0 && (
|
|
342
|
+
<SidebarMenuBadge aria-label={`${runningCount} running`}>{runningCount}</SidebarMenuBadge>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
<CollapsibleContent>
|
|
346
|
+
<SidebarMenuSub>
|
|
347
|
+
{shown.length === 0 && (
|
|
348
|
+
<SidebarMenuSubItem>
|
|
349
|
+
<span className="block px-2 py-1 text-xs text-muted-foreground">
|
|
350
|
+
No sessions yet
|
|
351
|
+
</span>
|
|
352
|
+
</SidebarMenuSubItem>
|
|
353
|
+
)}
|
|
354
|
+
{shown.map((session) => {
|
|
355
|
+
const title = displaySessionTitle(session.title);
|
|
356
|
+
const path = sessionPath(project.id, session.id);
|
|
357
|
+
const isActive = session.id === activeSessionId;
|
|
358
|
+
return (
|
|
359
|
+
<SidebarMenuSubItem key={session.id}>
|
|
360
|
+
<SidebarMenuSubButton
|
|
361
|
+
href={path}
|
|
362
|
+
onClick={go(path)}
|
|
363
|
+
isActive={isActive}
|
|
364
|
+
aria-current={isActive ? "page" : undefined}
|
|
365
|
+
title={title}
|
|
366
|
+
// Target sentuh lebih tinggi di HP (h-7 default terlalu kecil).
|
|
367
|
+
className="h-9 md:h-7"
|
|
368
|
+
>
|
|
369
|
+
<span
|
|
370
|
+
aria-hidden
|
|
371
|
+
className={cn(
|
|
372
|
+
"size-2 shrink-0 rounded-full",
|
|
373
|
+
SESSION_STATUS_DOT[session.status],
|
|
374
|
+
)}
|
|
375
|
+
/>
|
|
376
|
+
<span className="sr-only">{SESSION_STATUS_LABEL[session.status]}:</span>
|
|
377
|
+
<span>{title}</span>
|
|
378
|
+
</SidebarMenuSubButton>
|
|
379
|
+
{/* Aksi cepat khusus desktop (hover); HP pakai halaman Session. */}
|
|
380
|
+
{!isMobile && (
|
|
381
|
+
<SidebarSessionMenu
|
|
382
|
+
session={session}
|
|
383
|
+
title={title}
|
|
384
|
+
onDeleted={() => {
|
|
385
|
+
// Session yang sedang dibuka dihapus -> keluar ke Project.
|
|
386
|
+
if (isActive) navigate(detailPath);
|
|
387
|
+
}}
|
|
388
|
+
/>
|
|
389
|
+
)}
|
|
390
|
+
</SidebarMenuSubItem>
|
|
391
|
+
);
|
|
392
|
+
})}
|
|
393
|
+
{/* Selalu ada: jalan ke halaman Project untuk layar sentuh &
|
|
394
|
+
keyboard (dobel klik hanya pintasan mouse). */}
|
|
395
|
+
{/* Sengaja lebih ringan dari baris Session: teks 11px, tipis,
|
|
396
|
+
redup, tanpa titik status — terbaca sebagai tautan sekunder.
|
|
397
|
+
Tinggi sentuh tetap 32px di HP. */}
|
|
398
|
+
<SidebarMenuSubItem>
|
|
399
|
+
<SidebarMenuSubButton
|
|
400
|
+
href={detailPath}
|
|
401
|
+
onClick={go(detailPath)}
|
|
402
|
+
size="sm"
|
|
403
|
+
isActive={active && activeSessionId === null}
|
|
404
|
+
aria-current={active && activeSessionId === null ? "page" : undefined}
|
|
405
|
+
className="group/detail h-8 w-fit gap-0.5 pr-1.5 text-[11px] font-normal text-muted-foreground/80 hover:text-foreground data-[active=true]:font-medium data-[active=true]:text-foreground md:h-6 [&>svg]:size-3 [&>svg]:text-current"
|
|
406
|
+
>
|
|
407
|
+
<span>{hiddenCount > 0 ? `Show all ${sessions.length}` : "Project details"}</span>
|
|
408
|
+
<ChevronRightIcon
|
|
409
|
+
className="transition-transform group-hover/detail:translate-x-0.5"
|
|
410
|
+
aria-hidden
|
|
411
|
+
/>
|
|
412
|
+
</SidebarMenuSubButton>
|
|
413
|
+
</SidebarMenuSubItem>
|
|
414
|
+
</SidebarMenuSub>
|
|
415
|
+
</CollapsibleContent>
|
|
416
|
+
</SidebarMenuItem>
|
|
417
|
+
</Collapsible>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command palette pencarian Session (dibuka dari tombol "Search" di sidebar
|
|
3
|
+
* atau pintasan Ctrl/⌘+K).
|
|
4
|
+
*
|
|
5
|
+
* - Input di atas, daftar hasil di bawah: tiap baris = judul Session + nama
|
|
6
|
+
* Project. Query kosong -> Session terbaru dari seluruh Project.
|
|
7
|
+
* - Navigasi keyboard: ↑/↓ memindah sorotan, Enter membuka, Esc menutup.
|
|
8
|
+
* - Pencocokan memakai `filterSidebarGroups` (nama Project / judul Session).
|
|
9
|
+
*/
|
|
10
|
+
import { FolderIcon, MessageSquareIcon, SearchIcon } from "lucide-react";
|
|
11
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
+
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
|
|
13
|
+
import { sessionPath } from "@/lib/routes";
|
|
14
|
+
import { SESSION_STATUS_DOT, SESSION_STATUS_LABEL } from "@/lib/session-status";
|
|
15
|
+
import { displaySessionTitle } from "@/lib/session-title";
|
|
16
|
+
import { filterSidebarGroups, type SidebarProjectGroup } from "@/lib/sidebar-groups";
|
|
17
|
+
import { cn } from "@/lib/utils";
|
|
18
|
+
import type { Session } from "@/types";
|
|
19
|
+
|
|
20
|
+
/** Batas hasil yang dirender agar daftar tetap ringan. */
|
|
21
|
+
const MAX_RESULTS = 50;
|
|
22
|
+
|
|
23
|
+
interface SearchResult {
|
|
24
|
+
session: Session;
|
|
25
|
+
projectName: string;
|
|
26
|
+
title: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SearchDialogProps {
|
|
30
|
+
open: boolean;
|
|
31
|
+
onOpenChange: (open: boolean) => void;
|
|
32
|
+
/** Seluruh grup (belum difilter). */
|
|
33
|
+
groups: SidebarProjectGroup[];
|
|
34
|
+
onSelect: (path: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function SearchDialog({ open, onOpenChange, groups, onSelect }: SearchDialogProps) {
|
|
38
|
+
const [query, setQuery] = useState("");
|
|
39
|
+
const [highlight, setHighlight] = useState(0);
|
|
40
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
|
|
42
|
+
// Reset setiap kali dibuka.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (open) {
|
|
45
|
+
setQuery("");
|
|
46
|
+
setHighlight(0);
|
|
47
|
+
}
|
|
48
|
+
}, [open]);
|
|
49
|
+
|
|
50
|
+
const results = useMemo<SearchResult[]>(() => {
|
|
51
|
+
const titleOf = (s: Session) => displaySessionTitle(s.title);
|
|
52
|
+
const filtered = filterSidebarGroups(groups, query, titleOf);
|
|
53
|
+
const flat = filtered.flatMap((g) =>
|
|
54
|
+
g.sessions.map((session) => ({
|
|
55
|
+
session,
|
|
56
|
+
projectName: g.project.name,
|
|
57
|
+
title: titleOf(session),
|
|
58
|
+
})),
|
|
59
|
+
);
|
|
60
|
+
// Terbaru di atas lintas Project.
|
|
61
|
+
flat.sort((a, b) => b.session.updatedAt - a.session.updatedAt);
|
|
62
|
+
return flat.slice(0, MAX_RESULTS);
|
|
63
|
+
}, [groups, query]);
|
|
64
|
+
|
|
65
|
+
// Sorotan tetap valid saat hasil berubah.
|
|
66
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reset saat query berubah
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
setHighlight(0);
|
|
69
|
+
}, [query]);
|
|
70
|
+
|
|
71
|
+
// Gulirkan baris tersorot ke dalam viewport.
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
listRef.current
|
|
74
|
+
?.querySelector<HTMLElement>(`[data-index="${highlight}"]`)
|
|
75
|
+
?.scrollIntoView({ block: "nearest" });
|
|
76
|
+
}, [highlight]);
|
|
77
|
+
|
|
78
|
+
const choose = (r: SearchResult | undefined) => {
|
|
79
|
+
if (!r) return;
|
|
80
|
+
onOpenChange(false);
|
|
81
|
+
onSelect(sessionPath(r.session.projectId, r.session.id));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
85
|
+
if (e.key === "ArrowDown") {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
setHighlight((h) => Math.min(h + 1, results.length - 1));
|
|
88
|
+
} else if (e.key === "ArrowUp") {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
setHighlight((h) => Math.max(h - 1, 0));
|
|
91
|
+
} else if (e.key === "Enter") {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
choose(results[highlight]);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const trimmed = query.trim();
|
|
98
|
+
const activeId = results[highlight] ? `search-result-${highlight}` : undefined;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
102
|
+
<DialogContent
|
|
103
|
+
showCloseButton={false}
|
|
104
|
+
className="top-[15%] translate-y-0 gap-0 overflow-hidden p-0 sm:max-w-xl"
|
|
105
|
+
>
|
|
106
|
+
<DialogTitle className="sr-only">Search sessions</DialogTitle>
|
|
107
|
+
<DialogDescription className="sr-only">
|
|
108
|
+
Type to filter sessions by title or project name.
|
|
109
|
+
</DialogDescription>
|
|
110
|
+
|
|
111
|
+
<div className="flex items-center gap-2 border-b px-4">
|
|
112
|
+
<SearchIcon className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
113
|
+
<input
|
|
114
|
+
autoFocus
|
|
115
|
+
type="text"
|
|
116
|
+
role="combobox"
|
|
117
|
+
aria-expanded
|
|
118
|
+
aria-controls="search-results"
|
|
119
|
+
aria-activedescendant={activeId}
|
|
120
|
+
aria-label="Search sessions"
|
|
121
|
+
value={query}
|
|
122
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
123
|
+
onKeyDown={onKeyDown}
|
|
124
|
+
placeholder="Search sessions…"
|
|
125
|
+
className="h-12 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
126
|
+
/>
|
|
127
|
+
<kbd className="hidden shrink-0 rounded border bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground sm:inline">
|
|
128
|
+
Esc
|
|
129
|
+
</kbd>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div
|
|
133
|
+
ref={listRef}
|
|
134
|
+
id="search-results"
|
|
135
|
+
role="listbox"
|
|
136
|
+
aria-label="Results"
|
|
137
|
+
className="max-h-[min(60vh,420px)] overflow-y-auto p-2"
|
|
138
|
+
>
|
|
139
|
+
{results.length === 0 ? (
|
|
140
|
+
<p className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
141
|
+
{trimmed === "" ? "No sessions yet." : `No matches for “${trimmed}”.`}
|
|
142
|
+
</p>
|
|
143
|
+
) : (
|
|
144
|
+
<>
|
|
145
|
+
<p className="px-2 pt-1 pb-2 text-xs font-medium text-muted-foreground">
|
|
146
|
+
{trimmed === "" ? "Recent" : "Results"}
|
|
147
|
+
</p>
|
|
148
|
+
{results.map((r, i) => (
|
|
149
|
+
<div
|
|
150
|
+
key={r.session.id}
|
|
151
|
+
id={`search-result-${i}`}
|
|
152
|
+
data-index={i}
|
|
153
|
+
role="option"
|
|
154
|
+
aria-selected={i === highlight}
|
|
155
|
+
tabIndex={-1}
|
|
156
|
+
onMouseMove={() => setHighlight(i)}
|
|
157
|
+
onClick={() => choose(r)}
|
|
158
|
+
onKeyDown={() => {}}
|
|
159
|
+
className={cn(
|
|
160
|
+
"flex cursor-pointer items-center gap-3 rounded-md px-2 py-2 text-sm",
|
|
161
|
+
i === highlight && "bg-accent text-accent-foreground",
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
<MessageSquareIcon
|
|
165
|
+
className="size-4 shrink-0 text-muted-foreground"
|
|
166
|
+
aria-hidden
|
|
167
|
+
/>
|
|
168
|
+
<span className="min-w-0 flex-1 truncate">{r.title}</span>
|
|
169
|
+
<span className="flex max-w-[40%] shrink-0 items-center gap-1 truncate text-xs text-muted-foreground">
|
|
170
|
+
<FolderIcon className="size-3 shrink-0" aria-hidden />
|
|
171
|
+
<span className="truncate">{r.projectName}</span>
|
|
172
|
+
</span>
|
|
173
|
+
<span
|
|
174
|
+
aria-hidden
|
|
175
|
+
className={cn(
|
|
176
|
+
"size-2 shrink-0 rounded-full",
|
|
177
|
+
SESSION_STATUS_DOT[r.session.status],
|
|
178
|
+
)}
|
|
179
|
+
/>
|
|
180
|
+
<span className="sr-only">{SESSION_STATUS_LABEL[r.session.status]}</span>
|
|
181
|
+
</div>
|
|
182
|
+
))}
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</DialogContent>
|
|
187
|
+
</Dialog>
|
|
188
|
+
);
|
|
189
|
+
}
|