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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daftar model dengan pencarian — komponen presentasional bersama.
|
|
3
|
+
*
|
|
4
|
+
* Dipakai dua tempat dengan cara berbeda:
|
|
5
|
+
* - `ModelPicker.tsx` (ganti model Session berjalan) membukanya sebagai dialog.
|
|
6
|
+
* - `SessionList.tsx` (buat Session baru) menyematkannya di dalam form.
|
|
7
|
+
*
|
|
8
|
+
* Komponen ini tidak melakukan fetch maupun simpan: pemanggil menyediakan
|
|
9
|
+
* `models` dan menerima pilihan lewat `onSelect`. Aturan pencarian dan
|
|
10
|
+
* pengelompokan berasal dari `lib/model-picker.ts` (murni, teruji).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CheckIcon, SearchIcon } from "lucide-react";
|
|
14
|
+
import { useMemo, useState } from "react";
|
|
15
|
+
import { Input } from "@/components/ui/input";
|
|
16
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_MODEL_KEY,
|
|
19
|
+
describeModelCount,
|
|
20
|
+
filterModels,
|
|
21
|
+
groupModels,
|
|
22
|
+
optionKey,
|
|
23
|
+
} from "@/lib/model-picker";
|
|
24
|
+
import { cn } from "@/lib/utils";
|
|
25
|
+
import type { ModelOption } from "@/server/services/opencode-client";
|
|
26
|
+
import type { SessionModel } from "@/types";
|
|
27
|
+
|
|
28
|
+
export interface ModelSearchListProps {
|
|
29
|
+
/** Daftar model dari server; `null` = belum dimuat. */
|
|
30
|
+
models: ModelOption[] | null;
|
|
31
|
+
loading: boolean;
|
|
32
|
+
/** Kunci model yang sedang aktif/terpilih (lihat `keyOfSessionModel`). */
|
|
33
|
+
activeKey: string;
|
|
34
|
+
/** Pilihan baru; `null` = pakai model default opencode. */
|
|
35
|
+
onSelect: (model: SessionModel | null) => void;
|
|
36
|
+
/** Kunci aksi sementara pilihan sedang disimpan. */
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Tinggi area daftar (kelas Tailwind); dialog memakai flex-1. */
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ModelSearchList({
|
|
43
|
+
models,
|
|
44
|
+
loading,
|
|
45
|
+
activeKey,
|
|
46
|
+
onSelect,
|
|
47
|
+
disabled = false,
|
|
48
|
+
className,
|
|
49
|
+
}: ModelSearchListProps) {
|
|
50
|
+
const [query, setQuery] = useState("");
|
|
51
|
+
|
|
52
|
+
const filtered = useMemo(() => filterModels(models ?? [], query), [models, query]);
|
|
53
|
+
const groups = useMemo(() => groupModels(filtered), [filtered]);
|
|
54
|
+
/** Opsi "default" ikut disaring agar hasil pencarian konsisten. */
|
|
55
|
+
const showDefault = useMemo(() => {
|
|
56
|
+
const q = query.trim().toLowerCase();
|
|
57
|
+
return q === "" || "model default".includes(q);
|
|
58
|
+
}, [query]);
|
|
59
|
+
const empty = !loading && !showDefault && groups.length === 0;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={cn("flex min-h-0 flex-col gap-2", className)}>
|
|
63
|
+
<div className="relative shrink-0">
|
|
64
|
+
<SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
65
|
+
<Input
|
|
66
|
+
value={query}
|
|
67
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
68
|
+
placeholder="Search models or providers…"
|
|
69
|
+
aria-label="Search models"
|
|
70
|
+
className="pl-9"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
75
|
+
{loading ? (
|
|
76
|
+
<div className="flex items-center justify-center gap-2 py-8 text-sm text-muted-foreground">
|
|
77
|
+
<Spinner className="size-4" />
|
|
78
|
+
Loading models…
|
|
79
|
+
</div>
|
|
80
|
+
) : empty ? (
|
|
81
|
+
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
82
|
+
No models match “{query.trim()}”.
|
|
83
|
+
</p>
|
|
84
|
+
) : (
|
|
85
|
+
<div className="flex flex-col gap-1 pb-1">
|
|
86
|
+
{showDefault && (
|
|
87
|
+
<ModelRow
|
|
88
|
+
name="Default model"
|
|
89
|
+
hint="Follow opencode configuration"
|
|
90
|
+
selected={activeKey === DEFAULT_MODEL_KEY}
|
|
91
|
+
disabled={disabled}
|
|
92
|
+
onSelect={() => onSelect(null)}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
{groups.map((g) => (
|
|
96
|
+
<div key={g.providerID} className="flex flex-col gap-1">
|
|
97
|
+
<p className="px-2 pt-2 text-xs font-medium text-muted-foreground">
|
|
98
|
+
{g.providerName}
|
|
99
|
+
</p>
|
|
100
|
+
{g.models.map((m) => (
|
|
101
|
+
<ModelRow
|
|
102
|
+
key={optionKey(m)}
|
|
103
|
+
name={m.name}
|
|
104
|
+
hint={m.modelID}
|
|
105
|
+
selected={activeKey === optionKey(m)}
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
onSelect={() => onSelect({ providerID: m.providerID, modelID: m.modelID })}
|
|
108
|
+
/>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<p className="shrink-0 text-xs text-muted-foreground">
|
|
117
|
+
{loading ? "Loading…" : describeModelCount(filtered.length)}
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface ModelRowProps {
|
|
124
|
+
name: string;
|
|
125
|
+
hint: string;
|
|
126
|
+
selected: boolean;
|
|
127
|
+
disabled: boolean;
|
|
128
|
+
onSelect: () => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Satu baris model — `min-h-11` memberi target sentuh nyaman di HP. */
|
|
132
|
+
function ModelRow({ name, hint, selected, disabled, onSelect }: ModelRowProps) {
|
|
133
|
+
return (
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={onSelect}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
aria-current={selected}
|
|
139
|
+
className={cn(
|
|
140
|
+
"flex min-h-11 w-full items-center gap-2 rounded-md px-2 py-2 text-left transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none disabled:opacity-50",
|
|
141
|
+
selected && "bg-accent",
|
|
142
|
+
)}
|
|
143
|
+
>
|
|
144
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
145
|
+
<span className="truncate text-sm font-medium">{name}</span>
|
|
146
|
+
<span className="truncate font-mono text-[11px] text-muted-foreground">{hint}</span>
|
|
147
|
+
</span>
|
|
148
|
+
{selected && <CheckIcon className="size-4 shrink-0 text-primary" />}
|
|
149
|
+
</button>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form pembuatan Session baru (dari daftar Session / halaman Project).
|
|
3
|
+
*
|
|
4
|
+
* Pilih tipe CLI_Agent (`opencode` | `claude-code`) + model LLM
|
|
5
|
+
* (`GET /api/projects/:id/models`, dimuat malas saat dialog dibuka) lalu
|
|
6
|
+
* `POST /api/sessions` — direktori kerja memakai path Project
|
|
7
|
+
* (Requirement 10.9), tipe tidak didukung ditolak (1.3). Di layar HP dialog
|
|
8
|
+
* ini tampil sebagai dialog agar tidak mendorong daftar ke bawah.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useState } from "react";
|
|
11
|
+
import { toast } from "sonner";
|
|
12
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
13
|
+
import { Button } from "@/components/ui/button";
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogFooter,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
} from "@/components/ui/dialog";
|
|
22
|
+
import { Field, FieldLabel } from "@/components/ui/field";
|
|
23
|
+
import {
|
|
24
|
+
Select,
|
|
25
|
+
SelectContent,
|
|
26
|
+
SelectItem,
|
|
27
|
+
SelectTrigger,
|
|
28
|
+
SelectValue,
|
|
29
|
+
} from "@/components/ui/select";
|
|
30
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
31
|
+
import { ApiError, apiFetch } from "@/lib/api";
|
|
32
|
+
import { keyOfSessionModel } from "@/lib/model-picker";
|
|
33
|
+
import type { ModelOption } from "@/server/services/opencode-client";
|
|
34
|
+
import type { AgentType, SessionModel } from "@/types";
|
|
35
|
+
import { ModelSearchList } from "./ModelSearchList";
|
|
36
|
+
|
|
37
|
+
// v1 headless: hanya opencode (claude-code punya mekanisme headless sendiri).
|
|
38
|
+
const AGENT_TYPES: AgentType[] = ["opencode"];
|
|
39
|
+
|
|
40
|
+
export interface NewSessionDialogProps {
|
|
41
|
+
open: boolean;
|
|
42
|
+
onOpenChange: (open: boolean) => void;
|
|
43
|
+
projectId: string;
|
|
44
|
+
/** Panggil setelah Session berhasil dibuat (memuat ulang daftar). */
|
|
45
|
+
onCreated: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function NewSessionDialog({
|
|
49
|
+
open,
|
|
50
|
+
onOpenChange,
|
|
51
|
+
projectId,
|
|
52
|
+
onCreated,
|
|
53
|
+
}: NewSessionDialogProps) {
|
|
54
|
+
const [agentType, setAgentType] = useState<AgentType>("opencode");
|
|
55
|
+
/** Model pilihan untuk Session baru; null = default opencode. */
|
|
56
|
+
const [selectedModel, setSelectedModel] = useState<SessionModel | null>(null);
|
|
57
|
+
const [models, setModels] = useState<ModelOption[] | null>(null);
|
|
58
|
+
const [modelsLoading, setModelsLoading] = useState(false);
|
|
59
|
+
const [modelsError, setModelsError] = useState<string | null>(null);
|
|
60
|
+
const [creating, setCreating] = useState(false);
|
|
61
|
+
const [createError, setCreateError] = useState<string | null>(null);
|
|
62
|
+
|
|
63
|
+
// Daftar model selalu di-fetch ulang saat dialog dibuka (bukan cache
|
|
64
|
+
// sekali pakai) agar perubahan config opencode terlihat tanpa reload
|
|
65
|
+
// halaman. Server headless hanya di-spawn saat endpoint ini dipanggil.
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!open) return;
|
|
68
|
+
setCreateError(null);
|
|
69
|
+
setModelsLoading(true);
|
|
70
|
+
setModelsError(null);
|
|
71
|
+
apiFetch(`/api/projects/${projectId}/models`)
|
|
72
|
+
.then(async (res) => {
|
|
73
|
+
const body = (await res.json()) as { models: ModelOption[] };
|
|
74
|
+
setModels(body.models);
|
|
75
|
+
})
|
|
76
|
+
.catch((e: unknown) => {
|
|
77
|
+
setModelsError(e instanceof ApiError ? e.message : "Failed to load models");
|
|
78
|
+
setModels([]);
|
|
79
|
+
})
|
|
80
|
+
.finally(() => {
|
|
81
|
+
setModelsLoading(false);
|
|
82
|
+
});
|
|
83
|
+
}, [open, projectId]);
|
|
84
|
+
|
|
85
|
+
const create = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
setCreating(true);
|
|
88
|
+
setCreateError(null);
|
|
89
|
+
try {
|
|
90
|
+
await apiFetch("/api/sessions", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: JSON.stringify({ agentType, projectId, model: selectedModel }),
|
|
93
|
+
});
|
|
94
|
+
onOpenChange(false);
|
|
95
|
+
onCreated();
|
|
96
|
+
toast.success("Session created.");
|
|
97
|
+
} catch (e) {
|
|
98
|
+
setCreateError(e instanceof ApiError ? e.message : "Failed to create session");
|
|
99
|
+
} finally {
|
|
100
|
+
setCreating(false);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Dialog
|
|
106
|
+
open={open}
|
|
107
|
+
onOpenChange={(next) => {
|
|
108
|
+
onOpenChange(next);
|
|
109
|
+
if (!next) setCreateError(null);
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{/* Tinggi dipatok: daftar model bisa panjang, jadi ia yang men-scroll
|
|
113
|
+
di dalam dialog — bukan dialog yang tumbuh melewati layar HP. */}
|
|
114
|
+
<DialogContent className="flex h-[80dvh] max-h-[620px] flex-col sm:max-w-md">
|
|
115
|
+
<DialogHeader>
|
|
116
|
+
<DialogTitle>New session</DialogTitle>
|
|
117
|
+
<DialogDescription>
|
|
118
|
+
The session runs on a headless OpenCode server with the project path as working
|
|
119
|
+
directory.
|
|
120
|
+
</DialogDescription>
|
|
121
|
+
</DialogHeader>
|
|
122
|
+
<form onSubmit={create} className="flex min-h-0 flex-1 flex-col gap-4">
|
|
123
|
+
<Field>
|
|
124
|
+
<FieldLabel htmlFor="agent-type">CLI_Agent type</FieldLabel>
|
|
125
|
+
<Select value={agentType} onValueChange={(v) => setAgentType(v as AgentType)}>
|
|
126
|
+
<SelectTrigger id="agent-type" className="w-full">
|
|
127
|
+
<SelectValue placeholder="Choose type" />
|
|
128
|
+
</SelectTrigger>
|
|
129
|
+
<SelectContent>
|
|
130
|
+
{AGENT_TYPES.map((t) => (
|
|
131
|
+
<SelectItem key={t} value={t}>
|
|
132
|
+
{t}
|
|
133
|
+
</SelectItem>
|
|
134
|
+
))}
|
|
135
|
+
</SelectContent>
|
|
136
|
+
</Select>
|
|
137
|
+
</Field>
|
|
138
|
+
|
|
139
|
+
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
|
140
|
+
<FieldLabel>Model</FieldLabel>
|
|
141
|
+
{modelsError ? (
|
|
142
|
+
<Alert variant="destructive">
|
|
143
|
+
<AlertTitle>Failed to load models</AlertTitle>
|
|
144
|
+
<AlertDescription>{modelsError}</AlertDescription>
|
|
145
|
+
</Alert>
|
|
146
|
+
) : (
|
|
147
|
+
<ModelSearchList
|
|
148
|
+
models={models}
|
|
149
|
+
loading={modelsLoading}
|
|
150
|
+
activeKey={keyOfSessionModel(selectedModel)}
|
|
151
|
+
onSelect={setSelectedModel}
|
|
152
|
+
disabled={creating}
|
|
153
|
+
className="flex-1"
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{createError && (
|
|
159
|
+
<Alert variant="destructive">
|
|
160
|
+
<AlertTitle>Failed to create session</AlertTitle>
|
|
161
|
+
<AlertDescription>{createError}</AlertDescription>
|
|
162
|
+
</Alert>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
<DialogFooter>
|
|
166
|
+
<Button
|
|
167
|
+
type="button"
|
|
168
|
+
variant="ghost"
|
|
169
|
+
onClick={() => onOpenChange(false)}
|
|
170
|
+
disabled={creating}
|
|
171
|
+
>
|
|
172
|
+
Cancel
|
|
173
|
+
</Button>
|
|
174
|
+
<Button type="submit" disabled={creating}>
|
|
175
|
+
{creating ? (
|
|
176
|
+
<>
|
|
177
|
+
<Spinner data-icon="inline-start" />
|
|
178
|
+
Creating…
|
|
179
|
+
</>
|
|
180
|
+
) : (
|
|
181
|
+
"Create session"
|
|
182
|
+
)}
|
|
183
|
+
</Button>
|
|
184
|
+
</DialogFooter>
|
|
185
|
+
</form>
|
|
186
|
+
</DialogContent>
|
|
187
|
+
</Dialog>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lampiran yang akan dikirim bersama pesan berikutnya, tampil DI DALAM kotak
|
|
3
|
+
* composer (di atas textarea) — jelas bahwa file ikut pesan ini.
|
|
4
|
+
*
|
|
5
|
+
* - Gambar: thumbnail kotak; klik = pratinjau layar penuh (`ImageLightbox`).
|
|
6
|
+
* - File lain: kartu ikon + nama + jenis · ukuran.
|
|
7
|
+
* - Tombol hapus selalu terlihat (layar sentuh tidak punya hover).
|
|
8
|
+
* - Baris bisa di-scroll horizontal bila lampiran banyak.
|
|
9
|
+
* - `hiddenKeys`: item yang sedang "mendarat" dari animasi plate drop —
|
|
10
|
+
* tetap menempati tempatnya (diukur sebagai tujuan animasi) tapi belum
|
|
11
|
+
* terlihat; `registerEl` memberi elemen tiap item ke pemanggil.
|
|
12
|
+
*
|
|
13
|
+
* Object URL dibuat & dilepas oleh pemilik state (`SessionComposer`).
|
|
14
|
+
*/
|
|
15
|
+
import { XIcon } from "lucide-react";
|
|
16
|
+
import { AnimatePresence } from "motion/react";
|
|
17
|
+
import * as m from "motion/react-m";
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { FileTile } from "@/components/sessions/AttachmentTile";
|
|
20
|
+
import { ImageLightbox } from "@/components/sessions/ImageLightbox";
|
|
21
|
+
import { cn } from "@/lib/utils";
|
|
22
|
+
|
|
23
|
+
export interface PendingAttachment {
|
|
24
|
+
key: string;
|
|
25
|
+
file: File;
|
|
26
|
+
/** Object URL pratinjau — hanya untuk gambar yang bisa dipratinjau. */
|
|
27
|
+
previewUrl: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Ukuran kotak (dipakai juga oleh plate agar mendarat pas di tempatnya). */
|
|
31
|
+
export const IMAGE_TILE_CLASS = "size-16 sm:size-[4.5rem]";
|
|
32
|
+
export const FILE_TILE_CLASS = "h-16 w-48 sm:h-[4.5rem]";
|
|
33
|
+
|
|
34
|
+
/** Isi visual satu lampiran tanpa tombol (dipakai list & plate). */
|
|
35
|
+
export function AttachmentVisual({ item }: { item: PendingAttachment }) {
|
|
36
|
+
if (item.previewUrl) {
|
|
37
|
+
return (
|
|
38
|
+
<span className={cn("block overflow-hidden rounded-lg border bg-muted", IMAGE_TILE_CLASS)}>
|
|
39
|
+
<img src={item.previewUrl} alt="" className="size-full object-cover" draggable={false} />
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return (
|
|
44
|
+
<FileTile
|
|
45
|
+
filename={item.file.name}
|
|
46
|
+
mime={item.file.type}
|
|
47
|
+
size={item.file.size}
|
|
48
|
+
className={FILE_TILE_CLASS}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PendingAttachmentsProps {
|
|
54
|
+
items: PendingAttachment[];
|
|
55
|
+
onRemove: (key: string) => void;
|
|
56
|
+
hiddenKeys?: ReadonlySet<string>;
|
|
57
|
+
registerEl?: (key: string, el: HTMLElement | null) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function PendingAttachments({
|
|
61
|
+
items,
|
|
62
|
+
onRemove,
|
|
63
|
+
hiddenKeys,
|
|
64
|
+
registerEl,
|
|
65
|
+
}: PendingAttachmentsProps) {
|
|
66
|
+
const [preview, setPreview] = useState<number | null>(null);
|
|
67
|
+
const images = items.filter((it) => it.previewUrl !== null);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<ul
|
|
72
|
+
aria-label={`${items.length} ${items.length === 1 ? "attachment" : "attachments"}`}
|
|
73
|
+
className="flex gap-2 overflow-x-auto px-1 pt-1 pb-2 [scrollbar-width:thin]"
|
|
74
|
+
>
|
|
75
|
+
<AnimatePresence initial={false}>
|
|
76
|
+
{items.map((it) => {
|
|
77
|
+
const hidden = hiddenKeys?.has(it.key) ?? false;
|
|
78
|
+
const imageIndex = it.previewUrl ? images.indexOf(it) : -1;
|
|
79
|
+
return (
|
|
80
|
+
<m.li
|
|
81
|
+
key={it.key}
|
|
82
|
+
ref={(el) => registerEl?.(it.key, el)}
|
|
83
|
+
// Item hasil drop sudah "dianimasikan" plate — muncul tanpa
|
|
84
|
+
// animasi masuk agar tidak ada kedipan saat plate berganti.
|
|
85
|
+
initial={hidden ? false : { opacity: 0, scale: 0.85 }}
|
|
86
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
87
|
+
exit={{ opacity: 0, scale: 0.85, transition: { duration: 0.15 } }}
|
|
88
|
+
transition={{ type: "spring", stiffness: 500, damping: 32 }}
|
|
89
|
+
className={cn("relative shrink-0", hidden && "invisible")}
|
|
90
|
+
>
|
|
91
|
+
{it.previewUrl ? (
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={() => setPreview(imageIndex)}
|
|
95
|
+
aria-label={`Preview ${it.file.name}`}
|
|
96
|
+
className="block rounded-lg outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
|
97
|
+
>
|
|
98
|
+
<AttachmentVisual item={it} />
|
|
99
|
+
</button>
|
|
100
|
+
) : (
|
|
101
|
+
<span title={it.file.name} className="block">
|
|
102
|
+
<AttachmentVisual item={it} />
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={() => onRemove(it.key)}
|
|
108
|
+
aria-label={`Remove ${it.file.name}`}
|
|
109
|
+
title="Remove"
|
|
110
|
+
className="absolute top-1 right-1 flex size-6 items-center justify-center rounded-full bg-black/60 text-white shadow-sm backdrop-blur-sm transition-colors outline-none hover:bg-black/80 focus-visible:ring-[3px] focus-visible:ring-white/60"
|
|
111
|
+
>
|
|
112
|
+
<XIcon className="size-3.5" aria-hidden />
|
|
113
|
+
</button>
|
|
114
|
+
</m.li>
|
|
115
|
+
);
|
|
116
|
+
})}
|
|
117
|
+
</AnimatePresence>
|
|
118
|
+
</ul>
|
|
119
|
+
|
|
120
|
+
<ImageLightbox
|
|
121
|
+
images={images.map((it) => ({ src: it.previewUrl as string, alt: it.file.name }))}
|
|
122
|
+
index={preview}
|
|
123
|
+
onIndexChange={setPreview}
|
|
124
|
+
/>
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
}
|