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.
- package/README.md +2 -0
- package/package.json +2 -1
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +170 -8
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +69 -6
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +174 -21
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +876 -159
- package/src/cli/index.ts +10 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +6 -6
- package/src/components/layout/AppShell.tsx +8 -3
- package/src/components/projects/HomeChat.tsx +85 -48
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/ProjectInstructions.tsx +27 -38
- package/src/components/projects/ProjectSidePanel.tsx +382 -316
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentPicker.tsx +13 -4
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +27 -7
- package/src/components/sessions/ContextPanel.tsx +159 -59
- package/src/components/sessions/ModelPicker.tsx +65 -27
- package/src/components/sessions/ModelSearchList.tsx +137 -57
- package/src/components/sessions/NewSessionDialog.tsx +100 -117
- package/src/components/sessions/SessionComposer.tsx +2 -1
- package/src/components/sessions/SessionHeader.tsx +1 -0
- package/src/components/sessions/SessionList.tsx +173 -66
- package/src/components/sessions/SessionRow.tsx +176 -76
- package/src/components/sessions/SessionView.tsx +17 -13
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +36 -0
- package/src/components/settings/LhrRemoteAccess.tsx +20 -25
- package/src/components/settings/RemoteAccessGroup.tsx +242 -267
- package/src/components/settings/prefs.tsx +69 -1
- package/src/components/skills/InstallNotifications.tsx +4 -3
- package/src/components/skills/InstallSkillDialog.tsx +256 -99
- package/src/components/skills/SkillCards.tsx +52 -8
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/config.ts +32 -3
- package/src/hooks/useAgentPicker.ts +6 -2
- package/src/hooks/useSessionList.ts +95 -2
- package/src/lib/__tests__/project-extensions.test.ts +32 -1
- package/src/lib/__tests__/project-overview.test.ts +15 -0
- package/src/lib/__tests__/routes.test.ts +2 -2
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/skill-installs.test.ts +1 -0
- package/src/lib/__tests__/skills-catalog.test.ts +36 -8
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/project-extensions.ts +66 -4
- package/src/lib/project-overview.ts +14 -8
- package/src/lib/routes.ts +13 -4
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/skill-installs.ts +3 -1
- package/src/lib/skills-catalog.ts +56 -17
- package/src/pages/projects/ProjectDetailPage.tsx +13 -12
- package/src/pages/settings/SettingsPage.tsx +212 -145
- package/src/pages/settings/__tests__/SettingsPage.test.ts +11 -4
- package/src/pages/skills/SkillsPage.tsx +89 -45
- package/src/server/__tests__/app.test.ts +15 -2
- package/src/server/app.ts +13 -1
- package/src/server/routes/projects.routes.ts +14 -4
- package/src/server/routes/sessions.routes.ts +1 -1
- package/src/server/routes/skills.routes.ts +24 -5
- 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__/session-manager.test.ts +40 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +22 -3
- package/src/server/services/__tests__/skills-registry.test.ts +6 -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/session-manager.ts +117 -52
- package/src/server/services/skill-install-jobs.ts +21 -8
- package/src/server/services/skills-registry.ts +18 -6
- package/src/types/message.ts +1 -1
- package/src/components/projects/PanelSection.tsx +0 -102
|
@@ -1,103 +1,119 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
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).
|
|
8
|
-
*
|
|
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 {
|
|
23
|
+
import { Dialog } from "@/components/ui/dialog";
|
|
14
24
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
/**
|
|
45
|
-
|
|
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>(
|
|
55
|
-
/**
|
|
56
|
-
const [
|
|
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
|
-
//
|
|
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
|
-
|
|
91
|
+
return () => {
|
|
92
|
+
stale = true;
|
|
93
|
+
};
|
|
94
|
+
}, [open, projectId, agentType]);
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
145
|
+
{createError && (
|
|
146
|
+
<Alert variant="destructive">
|
|
147
|
+
<AlertTitle>Failed to create session</AlertTitle>
|
|
148
|
+
<AlertDescription>{createError}</AlertDescription>
|
|
149
|
+
</Alert>
|
|
150
|
+
)}
|
|
164
151
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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}
|