kcg-code 0.1.0 → 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.
Files changed (82) hide show
  1. package/README.md +2 -0
  2. package/package.json +2 -1
  3. package/src/cli/__tests__/daemon.test.ts +84 -0
  4. package/src/cli/__tests__/dashboard.test.ts +170 -8
  5. package/src/cli/__tests__/keep-awake.test.ts +44 -0
  6. package/src/cli/__tests__/port.test.ts +86 -0
  7. package/src/cli/args.ts +69 -6
  8. package/src/cli/attach.ts +154 -0
  9. package/src/cli/commands.ts +174 -21
  10. package/src/cli/daemon.ts +275 -0
  11. package/src/cli/dashboard.tsx +876 -159
  12. package/src/cli/index.ts +10 -0
  13. package/src/cli/keep-awake.ts +148 -0
  14. package/src/cli/port.ts +92 -0
  15. package/src/cli/theme.ts +6 -6
  16. package/src/components/layout/AppShell.tsx +8 -3
  17. package/src/components/projects/HomeChat.tsx +85 -48
  18. package/src/components/projects/HomeRecentSessions.tsx +90 -0
  19. package/src/components/projects/ProjectInstructions.tsx +27 -38
  20. package/src/components/projects/ProjectSidePanel.tsx +382 -316
  21. package/src/components/sessions/AgentLogo.tsx +42 -0
  22. package/src/components/sessions/AgentPicker.tsx +13 -4
  23. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +27 -7
  24. package/src/components/sessions/ContextPanel.tsx +159 -59
  25. package/src/components/sessions/ModelPicker.tsx +65 -27
  26. package/src/components/sessions/ModelSearchList.tsx +137 -57
  27. package/src/components/sessions/NewSessionDialog.tsx +100 -117
  28. package/src/components/sessions/SessionComposer.tsx +2 -1
  29. package/src/components/sessions/SessionHeader.tsx +1 -0
  30. package/src/components/sessions/SessionList.tsx +173 -66
  31. package/src/components/sessions/SessionRow.tsx +176 -76
  32. package/src/components/sessions/SessionView.tsx +17 -13
  33. package/src/components/sessions/__tests__/ContextPanel.test.tsx +36 -0
  34. package/src/components/settings/LhrRemoteAccess.tsx +20 -25
  35. package/src/components/settings/RemoteAccessGroup.tsx +242 -267
  36. package/src/components/settings/prefs.tsx +69 -1
  37. package/src/components/skills/InstallNotifications.tsx +4 -3
  38. package/src/components/skills/InstallSkillDialog.tsx +256 -99
  39. package/src/components/skills/SkillCards.tsx +52 -8
  40. package/src/components/ui/adw-dialog.tsx +114 -0
  41. package/src/components/ui/adw-toggle-group.tsx +75 -0
  42. package/src/components/ui/search-entry.tsx +70 -0
  43. package/src/config.ts +32 -3
  44. package/src/hooks/useAgentPicker.ts +6 -2
  45. package/src/hooks/useSessionList.ts +95 -2
  46. package/src/lib/__tests__/project-extensions.test.ts +32 -1
  47. package/src/lib/__tests__/project-overview.test.ts +15 -0
  48. package/src/lib/__tests__/routes.test.ts +2 -2
  49. package/src/lib/__tests__/session-selection.test.ts +41 -0
  50. package/src/lib/__tests__/skill-installs.test.ts +1 -0
  51. package/src/lib/__tests__/skills-catalog.test.ts +36 -8
  52. package/src/lib/agent-types.ts +49 -0
  53. package/src/lib/project-extensions.ts +66 -4
  54. package/src/lib/project-overview.ts +14 -8
  55. package/src/lib/routes.ts +13 -4
  56. package/src/lib/session-selection.ts +40 -0
  57. package/src/lib/skill-installs.ts +3 -1
  58. package/src/lib/skills-catalog.ts +56 -17
  59. package/src/pages/projects/ProjectDetailPage.tsx +13 -12
  60. package/src/pages/settings/SettingsPage.tsx +212 -145
  61. package/src/pages/settings/__tests__/SettingsPage.test.ts +11 -4
  62. package/src/pages/skills/SkillsPage.tsx +89 -45
  63. package/src/server/__tests__/app.test.ts +15 -2
  64. package/src/server/app.ts +13 -1
  65. package/src/server/routes/projects.routes.ts +14 -4
  66. package/src/server/routes/sessions.routes.ts +1 -1
  67. package/src/server/routes/skills.routes.ts +24 -5
  68. package/src/server/services/__tests__/claude-code.test.ts +609 -0
  69. package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
  70. package/src/server/services/__tests__/claude-skills.test.ts +63 -0
  71. package/src/server/services/__tests__/session-manager.test.ts +40 -0
  72. package/src/server/services/__tests__/skill-install-jobs.test.ts +22 -3
  73. package/src/server/services/__tests__/skills-registry.test.ts +6 -0
  74. package/src/server/services/claude-code-client.ts +1093 -0
  75. package/src/server/services/claude-code-translate.ts +370 -0
  76. package/src/server/services/claude-mcp.ts +122 -0
  77. package/src/server/services/claude-skills.ts +102 -0
  78. package/src/server/services/session-manager.ts +117 -52
  79. package/src/server/services/skill-install-jobs.ts +21 -8
  80. package/src/server/services/skills-registry.ts +18 -6
  81. package/src/types/message.ts +1 -1
  82. package/src/components/projects/PanelSection.tsx +0 -102
@@ -1,103 +1,119 @@
1
1
  /**
2
- * Form pembuatan Session baru (dari daftar Session / halaman Project).
2
+ * Dialog pembuatan Session baru (halaman detail Project) — gaya AdwDialog
3
+ * (libadwaita), sama dengan dialog Agent & Model:
4
+ * - Header bar: judul "New Session" + nama Project, tombol tutup bundar.
5
+ * - Isi: AdwToggleGroup agent (OpenCode / Claude Code), lalu daftar model
6
+ * (`GET /api/projects/:id/models?agent=`, dimuat saat dialog dibuka).
7
+ * - Klik model (atau Enter di kolom cari = hasil teratas) LANGSUNG membuat
8
+ * Session dengan agent & model itu — tanpa tombol Create. Selama dibuat,
9
+ * baris yang diklik menampilkan spinner dan daftar dikunci.
10
+ * - Di HP tampil sebagai bottom sheet (`AdwDialogContent`).
3
11
  *
4
- * Pilih tipe CLI_Agent (`opencode` | `claude-code`) + model LLM
5
- * (`GET /api/projects/:id/models`, dimuat malas saat dialog dibuka) lalu
12
+ * Agent awal = agent terakhir yang dipakai (sama dengan composer homepage).
6
13
  * `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.
14
+ * (Requirement 10.9), tipe tidak didukung ditolak (1.3). Setelah berhasil,
15
+ * pemanggil menerima Session baru (mis. untuk langsung membukanya).
9
16
  */
10
17
  import { useEffect, useState } from "react";
11
18
  import { toast } from "sonner";
19
+ import { AgentLogo } from "@/components/sessions/AgentLogo";
20
+ import { AdwDialogContent } from "@/components/ui/adw-dialog";
21
+ import { AdwToggleGroup } from "@/components/ui/adw-toggle-group";
12
22
  import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
13
- import { Button } from "@/components/ui/button";
23
+ import { Dialog } from "@/components/ui/dialog";
14
24
  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";
25
+ AGENT_TYPE_LABELS,
26
+ AGENT_TYPES,
27
+ agentQuery,
28
+ readPreferredAgentType,
29
+ storePreferredAgentType,
30
+ } from "@/lib/agent-types";
31
31
  import { ApiError, apiFetch } from "@/lib/api";
32
32
  import { keyOfSessionModel } from "@/lib/model-picker";
33
33
  import type { ModelOption } from "@/server/services/opencode-client";
34
- import type { AgentType, SessionModel } from "@/types";
34
+ import type { AgentType, Session, SessionModel } from "@/types";
35
35
  import { ModelSearchList } from "./ModelSearchList";
36
36
 
37
- // v1 headless: hanya opencode (claude-code punya mekanisme headless sendiri).
38
- const AGENT_TYPES: AgentType[] = ["opencode"];
39
-
40
37
  export interface NewSessionDialogProps {
41
38
  open: boolean;
42
39
  onOpenChange: (open: boolean) => void;
43
40
  projectId: string;
44
- /** Panggil setelah Session berhasil dibuat (memuat ulang daftar). */
45
- onCreated: () => void;
41
+ /** Subjudul header bar. */
42
+ projectName: string;
43
+ /** Dipanggil dengan Session yang baru dibuat. */
44
+ onCreated: (session: Session) => void;
46
45
  }
47
46
 
48
47
  export function NewSessionDialog({
49
48
  open,
50
49
  onOpenChange,
51
50
  projectId,
51
+ projectName,
52
52
  onCreated,
53
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);
54
+ const [agentType, setAgentType] = useState<AgentType>(readPreferredAgentType);
55
+ /** Kunci model yang sedang dipakai membuat Session (spinner di barisnya). */
56
+ const [pendingKey, setPendingKey] = useState<string | null>(null);
57
57
  const [models, setModels] = useState<ModelOption[] | null>(null);
58
58
  const [modelsLoading, setModelsLoading] = useState(false);
59
59
  const [modelsError, setModelsError] = useState<string | null>(null);
60
60
  const [creating, setCreating] = useState(false);
61
61
  const [createError, setCreateError] = useState<string | null>(null);
62
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.
63
+ // Dibuka lagi -> mulai dari agent terakhir dipakai.
66
64
  useEffect(() => {
67
65
  if (!open) return;
66
+ setAgentType(readPreferredAgentType());
68
67
  setCreateError(null);
68
+ }, [open]);
69
+
70
+ // Daftar model selalu di-fetch ulang saat dialog dibuka / agent berganti
71
+ // agar perubahan config terlihat tanpa reload halaman. Respons usang
72
+ // (agent sudah diganti lagi) dibuang.
73
+ useEffect(() => {
74
+ if (!open) return;
75
+ let stale = false;
69
76
  setModelsLoading(true);
70
77
  setModelsError(null);
71
- apiFetch(`/api/projects/${projectId}/models`)
78
+ apiFetch(`/api/projects/${projectId}/models${agentQuery(agentType)}`)
72
79
  .then(async (res) => {
73
80
  const body = (await res.json()) as { models: ModelOption[] };
74
- setModels(body.models);
81
+ if (!stale) setModels(body.models);
75
82
  })
76
83
  .catch((e: unknown) => {
84
+ if (stale) return;
77
85
  setModelsError(e instanceof ApiError ? e.message : "Failed to load models");
78
86
  setModels([]);
79
87
  })
80
88
  .finally(() => {
81
- setModelsLoading(false);
89
+ if (!stale) setModelsLoading(false);
82
90
  });
83
- }, [open, projectId]);
91
+ return () => {
92
+ stale = true;
93
+ };
94
+ }, [open, projectId, agentType]);
84
95
 
85
- const create = async (event: React.FormEvent<HTMLFormElement>) => {
86
- event.preventDefault();
96
+ /** Klik model = buat Session dengan agent & model ini, lalu buka. */
97
+ const create = async (model: SessionModel | null) => {
98
+ if (creating) return;
87
99
  setCreating(true);
100
+ setPendingKey(keyOfSessionModel(model));
88
101
  setCreateError(null);
89
102
  try {
90
- await apiFetch("/api/sessions", {
103
+ const res = await apiFetch("/api/sessions", {
91
104
  method: "POST",
92
- body: JSON.stringify({ agentType, projectId, model: selectedModel }),
105
+ body: JSON.stringify({ agentType, projectId, model }),
93
106
  });
107
+ const { session } = (await res.json()) as { session: Session };
108
+ storePreferredAgentType(agentType);
94
109
  onOpenChange(false);
95
- onCreated();
110
+ onCreated(session);
96
111
  toast.success("Session created.");
97
112
  } catch (e) {
98
113
  setCreateError(e instanceof ApiError ? e.message : "Failed to create session");
99
114
  } finally {
100
115
  setCreating(false);
116
+ setPendingKey(null);
101
117
  }
102
118
  };
103
119
 
@@ -105,85 +121,52 @@ export function NewSessionDialog({
105
121
  <Dialog
106
122
  open={open}
107
123
  onOpenChange={(next) => {
124
+ if (creating) return;
108
125
  onOpenChange(next);
109
- if (!next) setCreateError(null);
110
126
  }}
111
127
  >
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>
128
+ <AdwDialogContent
129
+ title="New Session"
130
+ subtitle={creating ? "Starting…" : projectName}
131
+ closeDisabled={creating}
132
+ >
133
+ <AdwToggleGroup
134
+ label="Agent"
135
+ value={agentType}
136
+ disabled={creating}
137
+ onChange={setAgentType}
138
+ options={AGENT_TYPES.map((t) => ({
139
+ value: t,
140
+ label: AGENT_TYPE_LABELS[t],
141
+ icon: <AgentLogo agentType={t} />,
142
+ }))}
143
+ />
157
144
 
158
- {createError && (
159
- <Alert variant="destructive">
160
- <AlertTitle>Failed to create session</AlertTitle>
161
- <AlertDescription>{createError}</AlertDescription>
162
- </Alert>
163
- )}
145
+ {createError && (
146
+ <Alert variant="destructive">
147
+ <AlertTitle>Failed to create session</AlertTitle>
148
+ <AlertDescription>{createError}</AlertDescription>
149
+ </Alert>
150
+ )}
164
151
 
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>
152
+ {modelsError ? (
153
+ <Alert variant="destructive">
154
+ <AlertTitle>Failed to load models</AlertTitle>
155
+ <AlertDescription>{modelsError}</AlertDescription>
156
+ </Alert>
157
+ ) : (
158
+ // Tanpa `activeKey`: tidak ada centang — tiap baris adalah aksi
159
+ // "mulai dengan model ini".
160
+ <ModelSearchList
161
+ models={models}
162
+ loading={modelsLoading}
163
+ pendingKey={pendingKey}
164
+ onSelect={(model) => void create(model)}
165
+ disabled={creating}
166
+ className="flex-1"
167
+ />
168
+ )}
169
+ </AdwDialogContent>
187
170
  </Dialog>
188
171
  );
189
172
  }
@@ -126,7 +126,7 @@ export function SessionComposer({
126
126
  * Fetch + pick daftar agent untuk Sheet aksi mobile — dipakai langsung
127
127
  * (tanpa nested popover) supaya tap satu mode langsung menerapkannya.
128
128
  */
129
- const agentPicker = useAgentPicker(session.projectId, session.id, setAgent);
129
+ const agentPicker = useAgentPicker(session.projectId, session.id, setAgent, session.agentType);
130
130
  // Muat daftar agent begitu Sheet aksi mobile pertama dibuka (lazy, sama
131
131
  // seperti popover AgentPicker di desktop).
132
132
  useEffect(() => {
@@ -589,6 +589,7 @@ export function SessionComposer({
589
589
  const agentPickerEl = (
590
590
  <AgentPicker
591
591
  projectId={session.projectId}
592
+ agentType={session.agentType}
592
593
  sessionId={session.id}
593
594
  agent={agent}
594
595
  onChanged={setAgent}
@@ -75,6 +75,7 @@ export function SessionHeader({
75
75
  <div className="flex min-w-0 flex-1 flex-col items-start">
76
76
  <ModelPicker
77
77
  projectId={session.projectId}
78
+ agentType={session.agentType}
78
79
  sessionId={session.id}
79
80
  model={model}
80
81
  onChanged={setModel}