project-knowledge 0.1.0 → 1.0.1
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/CHANGELOG.md +41 -0
- package/README.md +201 -58
- package/_site/_test/ai-profile-test.js +59 -1
- package/_site/_test/baseline-schema-test.js +4 -3
- package/_site/_test/claude-workbench-test.js +72 -0
- package/_site/_test/draft-apply-test.js +12 -6
- package/_site/_test/kb-v2-templates-test.js +31 -43
- package/_site/_test/knowledge-store-logs-supervision-test.js +143 -0
- package/_site/_test/package-startup-test.js +108 -0
- package/_site/_test/project-control-panel-task14-test.js +151 -0
- package/_site/_test/task15-20-integration-test.js +194 -0
- package/_site/_test/task15-20-ui-flow-test.js +144 -0
- package/_site/_test/ui-smoke-test.js +2 -2
- package/_site/index.html +1640 -90
- package/_site/lib/ai-adapter.js +3 -3
- package/_site/lib/ai-workspace.js +120 -0
- package/_site/lib/analysis-orchestrator.js +117 -32
- package/_site/lib/claude-cli-runner.js +862 -0
- package/_site/lib/context-pack-builder.js +19 -11
- package/_site/lib/draft-apply.js +80 -31
- package/_site/lib/index-builder.js +100 -0
- package/_site/lib/job-orchestrator.js +15 -11
- package/_site/lib/kb-v3.js +188 -0
- package/_site/lib/kb-validator.js +84 -0
- package/_site/lib/knowledge-store.js +141 -0
- package/_site/lib/llm-client.js +103 -56
- package/_site/lib/prompt-registry.js +102 -0
- package/_site/lib/structured-logger.js +120 -0
- package/_site/lib/supervision.js +103 -0
- package/_site/server.js +887 -30
- package/_site/vendor/tailwind-browser.js +947 -0
- package/_site/vendor/vue.global.prod.js +9 -0
- package/ai-profiles.json +13 -3
- package/bin/project-knowledge.js +51 -0
- package/docs/development-progress.md +141 -0
- package/package.json +11 -2
- package/scripts/gen-commit-doc.ps1 +1 -1
- package/scripts/list-features.ps1 +1 -1
- package/scripts/register-scheduled-task.bat +3 -1
package/_site/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>KB Control Center</title>
|
|
7
|
-
<script src="
|
|
8
|
-
<script src="
|
|
7
|
+
<script src="/vendor/vue.global.prod.js"></script>
|
|
8
|
+
<script src="/vendor/tailwind-browser.js"></script>
|
|
9
9
|
<style>
|
|
10
10
|
[v-cloak] { display: none; }
|
|
11
11
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; }
|
|
@@ -34,10 +34,26 @@
|
|
|
34
34
|
.btn-primary { border: 1px solid transparent; background: var(--accent); color: white; border-radius: 9px; padding: 8px 11px; font-size: 13px; }
|
|
35
35
|
.btn-primary:hover { background: var(--accent2); }
|
|
36
36
|
.btn-danger { border: 1px solid color-mix(in srgb, var(--bad) 35%, transparent); color: var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); border-radius: 9px; padding: 8px 11px; font-size: 13px; }
|
|
37
|
-
.input { background: var(--panel); border: 1px solid var(--line); color: var(--text); border-radius: 9px; padding: 9px 11px; }
|
|
37
|
+
.input { min-width: 0; box-sizing: border-box; background: var(--panel); border: 1px solid var(--line); color: var(--text); border-radius: 9px; padding: 9px 11px; }
|
|
38
|
+
label.grid .input { width: 100%; }
|
|
38
39
|
.dot { width: 8px; height: 8px; border-radius: 999px; display: inline-block; }
|
|
39
40
|
.good { background: var(--good); } .warn { background: var(--warn); } .bad { background: var(--bad); } .idle { background: var(--idle); }
|
|
40
41
|
.chip { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--line); border-radius: 999px; padding: 2px 8px; font-size: 11px; color: var(--muted); background: var(--panel2); }
|
|
42
|
+
.help-dot { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); background: var(--panel2); font-size: 11px; line-height: 1; cursor: help; }
|
|
43
|
+
.field-label { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
|
|
44
|
+
.markdown-body { font-size: 13px; line-height: 1.65; overflow-wrap: anywhere; }
|
|
45
|
+
.markdown-body > * + * { margin-top: 0.65rem; }
|
|
46
|
+
.markdown-body h1, .markdown-body h2, .markdown-body h3 { font-weight: 650; line-height: 1.25; }
|
|
47
|
+
.markdown-body h1 { font-size: 1.15rem; }
|
|
48
|
+
.markdown-body h2 { font-size: 1.05rem; }
|
|
49
|
+
.markdown-body h3 { font-size: 0.95rem; }
|
|
50
|
+
.markdown-body ul, .markdown-body ol { padding-left: 1.2rem; }
|
|
51
|
+
.markdown-body ul { list-style: disc; }
|
|
52
|
+
.markdown-body ol { list-style: decimal; }
|
|
53
|
+
.markdown-body code { border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; background: var(--panel2); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; }
|
|
54
|
+
.markdown-body pre { overflow: auto; border-radius: 8px; background: #020617; color: #e2e8f0; padding: 10px; }
|
|
55
|
+
.markdown-body pre code { border: 0; padding: 0; background: transparent; color: inherit; }
|
|
56
|
+
.markdown-body blockquote { border-left: 3px solid var(--line); padding-left: 10px; color: var(--muted); }
|
|
41
57
|
.scrollbar-thin::-webkit-scrollbar { width: 7px; height: 7px; }
|
|
42
58
|
.scrollbar-thin::-webkit-scrollbar-thumb { background: #73829666; border-radius: 999px; }
|
|
43
59
|
</style>
|
|
@@ -108,20 +124,91 @@
|
|
|
108
124
|
<option value="en">{{ t("english") }}</option>
|
|
109
125
|
</select>
|
|
110
126
|
<button @click="refreshAll" class="btn">{{ t("refresh") }}</button>
|
|
111
|
-
<button @click="activeView = 'import'" class="btn-primary">{{ t("importProject") }}</button>
|
|
112
127
|
</div>
|
|
113
128
|
</header>
|
|
114
129
|
|
|
115
130
|
<section v-if="activeView === 'dashboard'" class="space-y-5">
|
|
116
131
|
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
117
|
-
<
|
|
132
|
+
<button v-for="s in summaryCards" :key="s.label" @click="openSummaryPanel(s.key)"
|
|
133
|
+
class="panel rounded-xl border p-4 text-left transition hover:brightness-[0.98]">
|
|
118
134
|
<div class="text-sm muted">{{ s.label }}</div>
|
|
119
135
|
<div class="mt-2 text-2xl font-semibold">{{ s.value }}</div>
|
|
120
136
|
<div class="mt-1 text-xs muted">{{ s.note }}</div>
|
|
121
|
-
</
|
|
137
|
+
</button>
|
|
122
138
|
</div>
|
|
123
139
|
|
|
124
|
-
<
|
|
140
|
+
<section v-if="summaryPanel === 'pending'" class="panel rounded-xl border p-5">
|
|
141
|
+
<div class="mb-4 flex items-center justify-between gap-3">
|
|
142
|
+
<div>
|
|
143
|
+
<h2 class="text-lg font-semibold">{{ t("pendingCommitDetails") }}</h2>
|
|
144
|
+
<div class="mt-1 text-sm muted">{{ t("pendingCommitDetailsHelp") }}</div>
|
|
145
|
+
</div>
|
|
146
|
+
<button @click="loadPendingCommits" class="btn">{{ t("refresh") }}</button>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="grid gap-3">
|
|
149
|
+
<details v-for="item in pendingCommitItems" :key="item.slug" class="rounded-lg border p-3 panel2 group" :open="(item.pendingCount || 0) > 0 || !!item.error">
|
|
150
|
+
<summary class="flex cursor-pointer list-none flex-wrap items-center justify-between gap-2">
|
|
151
|
+
<div class="font-medium">{{ item.displayName }}</div>
|
|
152
|
+
<div class="flex items-center gap-2">
|
|
153
|
+
<span class="chip">{{ item.pendingCount }} {{ t("pending") }}</span>
|
|
154
|
+
<span class="muted transition group-open:rotate-90">›</span>
|
|
155
|
+
</div>
|
|
156
|
+
</summary>
|
|
157
|
+
<div class="mt-1 text-xs muted">{{ item.range || item.repoStatus || "-" }}</div>
|
|
158
|
+
<div v-if="item.error" class="mt-2 text-sm" style="color: var(--bad)">{{ item.error }}</div>
|
|
159
|
+
<div v-if="item.commits && item.commits.length" class="mt-3 overflow-auto">
|
|
160
|
+
<table class="w-full text-left text-xs">
|
|
161
|
+
<thead class="muted"><tr><th class="py-1 pr-3">Hash</th><th class="py-1 pr-3">{{ t("time") }}</th><th class="py-1 pr-3">Author</th><th class="py-1">{{ t("subject") }}</th></tr></thead>
|
|
162
|
+
<tbody>
|
|
163
|
+
<tr v-for="c in item.commits" :key="c.hash" class="border-t line">
|
|
164
|
+
<td class="py-1 pr-3 font-mono">{{ c.short || (c.hash || '').slice(0, 7) }}</td>
|
|
165
|
+
<td class="py-1 pr-3">{{ c.date || "-" }}</td>
|
|
166
|
+
<td class="py-1 pr-3">{{ c.author || "-" }}</td>
|
|
167
|
+
<td class="py-1">{{ c.subject || "-" }}</td>
|
|
168
|
+
</tr>
|
|
169
|
+
</tbody>
|
|
170
|
+
</table>
|
|
171
|
+
</div>
|
|
172
|
+
</details>
|
|
173
|
+
</div>
|
|
174
|
+
</section>
|
|
175
|
+
|
|
176
|
+
<section v-if="summaryPanel === 'issues'" class="panel rounded-xl border p-5">
|
|
177
|
+
<div class="mb-4 flex items-center justify-between gap-3">
|
|
178
|
+
<div>
|
|
179
|
+
<h2 class="text-lg font-semibold">{{ t("issueCenter") }}</h2>
|
|
180
|
+
<div class="mt-1 text-sm muted">{{ t("issueCenterHelp") }}</div>
|
|
181
|
+
</div>
|
|
182
|
+
<button @click="loadIssues" class="btn">{{ t("refresh") }}</button>
|
|
183
|
+
</div>
|
|
184
|
+
<div v-if="!supervisionIssues.length" class="rounded-lg border p-3 text-sm panel2">{{ t("noIssue") }}</div>
|
|
185
|
+
<div v-else class="grid gap-2">
|
|
186
|
+
<details v-for="group in issuesByProject" :key="group.slug" class="rounded-lg border p-3 panel2 group" :open="group.hasWarnOrError">
|
|
187
|
+
<summary class="flex cursor-pointer list-none flex-wrap items-center justify-between gap-2">
|
|
188
|
+
<div class="flex items-center gap-2">
|
|
189
|
+
<span :class="['dot', group.levelClass]"></span>
|
|
190
|
+
<span class="font-medium">{{ group.displayName }}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="flex items-center gap-2">
|
|
193
|
+
<span class="chip">{{ group.issues.length }}</span>
|
|
194
|
+
<span class="muted transition group-open:rotate-90">›</span>
|
|
195
|
+
</div>
|
|
196
|
+
</summary>
|
|
197
|
+
<div class="mt-3 grid gap-2">
|
|
198
|
+
<div v-for="issue in group.issues" :key="issue.issueId" class="rounded-lg border p-3 panel">
|
|
199
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
200
|
+
<span :class="['dot', issue.level === 'error' ? 'bad' : issue.level === 'warn' ? 'warn' : 'idle']"></span>
|
|
201
|
+
<span class="font-medium">{{ issue.message }}</span>
|
|
202
|
+
<span class="chip">{{ issue.source }}</span>
|
|
203
|
+
</div>
|
|
204
|
+
<pre v-if="issue.meta && Object.keys(issue.meta).length" class="mt-2 max-h-32 overflow-auto rounded-lg bg-slate-950 p-2 text-xs text-slate-100">{{ JSON.stringify(issue.meta, null, 2) }}</pre>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</details>
|
|
208
|
+
</div>
|
|
209
|
+
</section>
|
|
210
|
+
|
|
211
|
+
<div v-if="!summaryPanel" class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_390px]">
|
|
125
212
|
<section class="panel rounded-xl border p-5">
|
|
126
213
|
<div v-if="!selectedProject" class="py-20 text-center muted">{{ t("selectProject") }}</div>
|
|
127
214
|
<div v-else>
|
|
@@ -135,9 +222,10 @@
|
|
|
135
222
|
<div class="mt-2 break-all text-xs muted">{{ selectedProject.localPath }}</div>
|
|
136
223
|
</div>
|
|
137
224
|
<div class="flex flex-wrap gap-2">
|
|
138
|
-
<button @click="
|
|
139
|
-
<
|
|
140
|
-
<
|
|
225
|
+
<button @click="runKnowledgeUpdate(selectedSlug)" class="btn-primary">{{ t("runKnowledgeUpdate") }}</button>
|
|
226
|
+
<a :href="goalUrl(selectedSlug)" target="_blank" class="btn text-center">{{ t("editProjectGoal") }}</a>
|
|
227
|
+
<a v-if="isKbInit(selectedSlug)" :href="kbUrl(selectedSlug)" target="_blank" class="btn text-center">{{ t("openKb") }}</a>
|
|
228
|
+
<button @click="openRemoveProject(selectedSlug)" class="btn-danger">{{ t("removeProject") }}</button>
|
|
141
229
|
</div>
|
|
142
230
|
</div>
|
|
143
231
|
|
|
@@ -154,15 +242,25 @@
|
|
|
154
242
|
<h3 class="font-medium">{{ t("projectOperations") }}</h3>
|
|
155
243
|
<span class="chip">{{ selectedProject.aiProfileId || "mock-agent" }}</span>
|
|
156
244
|
</div>
|
|
157
|
-
<div class="grid gap-2
|
|
158
|
-
<button @click="
|
|
159
|
-
<button @click="
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<
|
|
165
|
-
<
|
|
245
|
+
<div class="grid gap-2">
|
|
246
|
+
<button @click="runKnowledgeUpdate(selectedSlug)" class="btn-primary">{{ t("runKnowledgeUpdate") }}</button>
|
|
247
|
+
<button v-if="!isKbInit(selectedSlug)" @click="initProject(selectedSlug)" class="btn">{{ t("initKb") }}</button>
|
|
248
|
+
<div class="grid gap-2 md:grid-cols-2">
|
|
249
|
+
<a :href="goalUrl(selectedSlug)" target="_blank" class="btn text-center">{{ t("editProjectGoal") }}</a>
|
|
250
|
+
<a v-if="isKbInit(selectedSlug)" :href="kbUrl(selectedSlug)" target="_blank" class="btn text-center">{{ t("openKb") }}</a>
|
|
251
|
+
</div>
|
|
252
|
+
<div v-if="actionMessage" class="rounded-lg border p-3 text-sm panel">{{ actionMessage }}</div>
|
|
253
|
+
<button @click="advancedOpen = !advancedOpen" class="btn text-left">{{ advancedOpen ? t("hideAdvanced") : t("showAdvanced") }}</button>
|
|
254
|
+
<div v-if="advancedOpen" class="grid gap-2 md:grid-cols-2">
|
|
255
|
+
<button @click="validateGit(selectedSlug)" class="btn">{{ t("validateGit") }}</button>
|
|
256
|
+
<button @click="scanProject(selectedSlug)" class="btn">{{ t("scan") }}</button>
|
|
257
|
+
<button @click="runJob('scan', selectedSlug)" class="btn">{{ t("runScanJob") }}</button>
|
|
258
|
+
<button @click="runJob('analyze-initial', selectedSlug)" class="btn">{{ t("initialAnalysis") }}</button>
|
|
259
|
+
<button @click="runJob('analyze-commits', selectedSlug)" class="btn">{{ t("commitAnalysis") }}</button>
|
|
260
|
+
<button @click="validateKb(selectedSlug)" class="btn">{{ t("validateKb") }}</button>
|
|
261
|
+
<button @click="migrateV2(selectedSlug)" class="btn">{{ t("migrateV2") }}</button>
|
|
262
|
+
<button @click="migrateV3(selectedSlug)" class="btn">{{ t("migrateV3") }}</button>
|
|
263
|
+
</div>
|
|
166
264
|
</div>
|
|
167
265
|
</div>
|
|
168
266
|
|
|
@@ -209,24 +307,98 @@
|
|
|
209
307
|
</section>
|
|
210
308
|
|
|
211
309
|
<aside class="space-y-5">
|
|
212
|
-
<section class="panel rounded-xl border p-
|
|
213
|
-
<div class="
|
|
214
|
-
<
|
|
215
|
-
|
|
310
|
+
<section class="panel rounded-xl border p-0 overflow-hidden flex flex-col" style="height: 78vh; min-height: 520px">
|
|
311
|
+
<div class="flex items-center justify-between border-b px-3 py-2 line">
|
|
312
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
313
|
+
<span :class="['dot shrink-0', terminalStatusDot]"></span>
|
|
314
|
+
<span class="text-sm font-semibold truncate">{{ t("claudeWorkbench") }}</span>
|
|
315
|
+
<span class="chip shrink-0">{{ terminalSession.state || 'idle' }}</span>
|
|
316
|
+
<span v-if="terminalSession.aiProfileId" class="chip shrink-0">{{ terminalSession.aiProfileId }}</span>
|
|
317
|
+
<span v-if="terminalSession.model" class="chip shrink-0">{{ terminalSession.model }}</span>
|
|
318
|
+
<span v-if="terminalSession.turns > 0" class="chip shrink-0">turn {{ terminalSession.turns }}</span>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
321
|
+
<button @click="startClaudeAnalysis" :disabled="!selectedSlug || terminalSession.state === 'running' || terminalSession.state === 'spawning'"
|
|
322
|
+
class="btn-primary text-xs px-2 py-1">{{ t("analyze") }}</button>
|
|
323
|
+
<button @click="restoreLatestClaudeSession(false)" :disabled="!selectedSlug || !terminalSessions.length"
|
|
324
|
+
class="btn text-xs px-2 py-1">{{ t("restoreSession") }}</button>
|
|
325
|
+
<button @click="stopClaudeSession" :disabled="!terminalSessionId || (terminalSession.state !== 'running' && terminalSession.state !== 'spawning' && terminalSession.state !== 'pending-permission')"
|
|
326
|
+
class="btn-danger text-xs px-2 py-1">{{ t("stop") }}</button>
|
|
327
|
+
<button @click="clearTerminal" class="btn text-xs px-2 py-1">{{ t("clear") }}</button>
|
|
328
|
+
<button @click="activeView = 'prompts'" class="btn text-xs px-2 py-1">{{ t("editPrompts") }}</button>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="grid grid-cols-3 gap-2 border-b px-3 py-2 text-[11px] line">
|
|
332
|
+
<div class="panel2 rounded-lg border px-2 py-1.5">
|
|
333
|
+
<div class="muted">{{ t("continuity") }}</div>
|
|
334
|
+
<div class="mt-1 flex items-center gap-1.5">
|
|
335
|
+
<span :class="['dot', terminalSession.claudeSessionId ? 'good' : 'idle']"></span>
|
|
336
|
+
<span class="truncate">{{ terminalSession.claudeSessionId ? terminalSession.claudeSessionId.slice(0, 10) : '-' }}</span>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
<div class="panel2 rounded-lg border px-2 py-1.5">
|
|
340
|
+
<div class="muted">{{ t("permissionStatus") }}</div>
|
|
341
|
+
<div class="mt-1 flex items-center gap-1.5">
|
|
342
|
+
<span :class="['dot', terminalPermission ? 'warn' : 'good']"></span>
|
|
343
|
+
<span class="truncate">{{ terminalPermission ? t("pendingReview") : t("approvedStatus") }}</span>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="panel2 rounded-lg border px-2 py-1.5">
|
|
347
|
+
<div class="muted">{{ t("recoveryStatus") }}</div>
|
|
348
|
+
<div class="mt-1 flex items-center gap-1.5">
|
|
349
|
+
<span :class="['dot', terminalSessions.length ? 'good' : 'idle']"></span>
|
|
350
|
+
<span class="truncate">{{ terminalSessions.length ? t("recoverable") : '-' }}</span>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
216
353
|
</div>
|
|
217
|
-
<div v-if="
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
354
|
+
<div v-if="terminalSession.state === 'running' || terminalSession.state === 'spawning'"
|
|
355
|
+
class="border-b px-3 py-1.5 text-[11px] flex items-center gap-2"
|
|
356
|
+
style="background: color-mix(in srgb, var(--warn) 12%, transparent); color: var(--warn)">
|
|
357
|
+
<span class="dot warn"></span>
|
|
358
|
+
<span>{{ t("claudeHasWriteAccess") }}</span>
|
|
359
|
+
</div>
|
|
360
|
+
<div ref="terminalScrollRef" class="flex-1 overflow-auto p-3 scrollbar-thin text-sm leading-relaxed" style="background: var(--panel2)">
|
|
361
|
+
<div v-if="!terminalLines.length" class="rounded-lg border px-3 py-8 text-center text-sm panel muted">{{ t("terminalEmpty") }}</div>
|
|
362
|
+
<div v-for="(line, i) in terminalLines" :key="i" class="mb-3">
|
|
363
|
+
<div v-if="line.kind === 'user'" class="flex justify-end">
|
|
364
|
+
<div class="max-w-[88%] rounded-xl border px-3 py-2 text-sm whitespace-pre-wrap" style="background: color-mix(in srgb, var(--accent) 12%, var(--panel)); border-color: color-mix(in srgb, var(--accent) 30%, var(--line));">{{ line.text }}</div>
|
|
365
|
+
</div>
|
|
366
|
+
<div v-else-if="line.kind === 'assistant'" class="max-w-[94%] rounded-xl border bg-[var(--panel)] px-3 py-2">
|
|
367
|
+
<div class="markdown-body" v-html="renderMarkdown(line.text)"></div>
|
|
222
368
|
</div>
|
|
223
|
-
<
|
|
224
|
-
<
|
|
369
|
+
<details v-else-if="line.kind === 'tool'" class="max-w-[94%] rounded-xl border bg-[var(--panel)] px-3 py-2 text-xs" :open="line.open">
|
|
370
|
+
<summary class="cursor-pointer select-none text-amber-600 dark:text-amber-300">
|
|
371
|
+
{{ line.name || 'tool' }} <span class="muted">{{ line.summary || '' }}</span>
|
|
372
|
+
</summary>
|
|
373
|
+
<pre v-if="line.inputJson" class="mt-2 overflow-auto rounded bg-slate-950 p-2 text-slate-100 whitespace-pre-wrap">{{ line.inputJson }}</pre>
|
|
374
|
+
</details>
|
|
375
|
+
<div v-else-if="line.kind === 'thinking'" class="max-w-[94%] rounded-lg border px-3 py-2 text-xs italic muted whitespace-pre-wrap" style="background: color-mix(in srgb, var(--accent) 6%, transparent)">{{ line.text || t("thinking") }}</div>
|
|
376
|
+
<div v-else-if="line.kind === 'permission'" class="max-w-[94%] rounded-xl border px-3 py-2 text-xs" style="background: color-mix(in srgb, var(--warn) 10%, var(--panel)); border-color: color-mix(in srgb, var(--warn) 35%, var(--line));">
|
|
377
|
+
<div class="flex items-start gap-2">
|
|
378
|
+
<span class="dot warn mt-1"></span>
|
|
379
|
+
<div class="min-w-0 flex-1">
|
|
380
|
+
<div class="font-semibold">{{ t("permissionRequired") }}</div>
|
|
381
|
+
<div class="mt-1 muted whitespace-pre-wrap">{{ line.summaryText || permissionSummaryText }}</div>
|
|
382
|
+
</div>
|
|
383
|
+
<div v-if="terminalPermission && terminalPermission.requestId === line.requestId" class="flex shrink-0 gap-1">
|
|
384
|
+
<button @click="approveClaudePermission(line.requestId)" :disabled="terminalPermissionBusy" class="btn-primary text-xs px-2 py-1">{{ t("approve") }}</button>
|
|
385
|
+
<button @click="denyClaudePermission(line.requestId)" :disabled="terminalPermissionBusy" class="btn-danger text-xs px-2 py-1">{{ t("deny") }}</button>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
225
388
|
</div>
|
|
226
|
-
<div class="
|
|
389
|
+
<div v-else-if="line.kind === 'error'" class="max-w-[94%] rounded-lg border px-3 py-2 text-xs text-red-500 whitespace-pre-wrap" style="background: color-mix(in srgb, var(--bad) 8%, var(--panel));">{{ line.text }}</div>
|
|
390
|
+
<div v-else class="text-center text-[11px] muted">{{ line.text }}</div>
|
|
227
391
|
</div>
|
|
228
392
|
</div>
|
|
229
|
-
<div
|
|
393
|
+
<div class="border-t p-2 flex gap-2 bg-slate-900 line">
|
|
394
|
+
<input v-model="terminalInput" @keyup.enter="sendClaudeInput"
|
|
395
|
+
:disabled="!terminalSessionId || terminalSession.claudeSessionId == null || terminalSession.state === 'pending-permission'"
|
|
396
|
+
:placeholder="terminalInputPlaceholder"
|
|
397
|
+
class="flex-1 rounded bg-slate-800 px-3 py-2 text-xs text-slate-100 font-mono border border-slate-700 focus:outline-none focus:border-slate-500" />
|
|
398
|
+
<button @click="sendClaudeInput"
|
|
399
|
+
:disabled="!terminalSessionId || !terminalInput.trim() || terminalSession.state === 'running' || terminalSession.state === 'spawning' || terminalSession.state === 'pending-permission'"
|
|
400
|
+
class="btn-primary text-xs px-3 py-2">{{ t("send") }}</button>
|
|
401
|
+
</div>
|
|
230
402
|
</section>
|
|
231
403
|
|
|
232
404
|
<section class="panel rounded-xl border p-5">
|
|
@@ -262,6 +434,27 @@
|
|
|
262
434
|
<label class="grid gap-1 text-sm">{{ t("gitPath") }}
|
|
263
435
|
<input v-model="form.gitPath" class="input font-mono text-xs" placeholder="Leave blank if same as local path" />
|
|
264
436
|
</label>
|
|
437
|
+
<div class="rounded-lg border p-3 panel2">
|
|
438
|
+
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
|
439
|
+
<div>
|
|
440
|
+
<div class="font-medium">{{ t("gitImportPreflight") }}</div>
|
|
441
|
+
<div class="mt-1 text-xs muted">{{ t("gitImportPreflightHelp") }}</div>
|
|
442
|
+
</div>
|
|
443
|
+
<button type="button" @click="runImportPreflight" class="btn">{{ t("check") }}</button>
|
|
444
|
+
</div>
|
|
445
|
+
<div v-if="importPreflight" class="mb-3 rounded-lg border p-3 text-sm panel">
|
|
446
|
+
<div>{{ t("repo") }}: {{ importPreflight.inspection && importPreflight.inspection.repoStatus || "-" }}</div>
|
|
447
|
+
<div v-if="importPreflight.error" style="color: var(--bad)">{{ importPreflight.error }}</div>
|
|
448
|
+
<div v-if="importPreflight.needsGitInit" style="color: var(--warn)">{{ t("needsGitInit") }}</div>
|
|
449
|
+
</div>
|
|
450
|
+
<div class="grid gap-3">
|
|
451
|
+
<label class="flex items-center gap-2 text-sm"><input type="checkbox" v-model="form.initGit" /> {{ t("initLocalGit") }}</label>
|
|
452
|
+
<label class="flex items-center gap-2 text-sm"><input type="checkbox" v-model="form.createInitialCommit" /> {{ t("createInitialCommit") }}</label>
|
|
453
|
+
<label class="grid gap-1 text-sm">{{ t("existingRemoteUrl") }}
|
|
454
|
+
<input v-model="form.remoteUrl" class="input font-mono text-xs" placeholder="https://github.com/org/repo.git" />
|
|
455
|
+
</label>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
265
458
|
<div class="grid gap-4 md:grid-cols-2">
|
|
266
459
|
<label class="grid gap-1 text-sm">{{ t("primaryLanguage") }}
|
|
267
460
|
<input v-model="form.primaryLanguage" class="input" placeholder="TypeScript" />
|
|
@@ -305,17 +498,93 @@
|
|
|
305
498
|
<h2 class="text-lg font-semibold">{{ t("aiProfiles") }}</h2>
|
|
306
499
|
<p class="mt-1 text-sm muted">{{ t("backedByAi") }}</p>
|
|
307
500
|
</div>
|
|
308
|
-
<button @click="
|
|
501
|
+
<button type="button" @click="saveCurrentProfile" class="btn-primary">{{ t("save") }}</button>
|
|
309
502
|
</div>
|
|
310
503
|
<div class="mt-5 grid gap-4">
|
|
311
|
-
<label class="grid gap-1 text-sm">
|
|
504
|
+
<label class="grid gap-1 text-sm">
|
|
505
|
+
<span class="field-label">{{ t("defaultProfile") }} <span class="help-dot" :title="t('defaultProfileHelp')" tabindex="0">?</span></span>
|
|
312
506
|
<select v-model="aiConfig.defaultProfileId" class="input">
|
|
313
|
-
<option v-for="p in aiConfig.profiles" :key="p.id" :value="p.id"
|
|
507
|
+
<option v-for="p in aiConfig.profiles" :key="p.id" :value="p.id" :disabled="p.enabled === false">{{ profileOptionLabel(p) }}</option>
|
|
314
508
|
</select>
|
|
315
509
|
</label>
|
|
510
|
+
|
|
511
|
+
<div class="panel2 rounded-xl border p-4">
|
|
512
|
+
<div class="mb-4 flex items-center justify-between gap-3">
|
|
513
|
+
<h3 class="font-medium">{{ t("profileEditor") }}</h3>
|
|
514
|
+
<div class="flex flex-wrap gap-2">
|
|
515
|
+
<button type="button" @click="addProfile" class="btn">{{ t("addProfile") }}</button>
|
|
516
|
+
<button type="button" @click="deleteCurrentProfile" class="btn-danger">{{ t("deleteProfile") }}</button>
|
|
517
|
+
<button type="button" @click="saveCurrentProfile" class="btn">{{ t("saveProfileForm") }}</button>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
521
|
+
<label class="grid gap-1 text-sm">
|
|
522
|
+
<span class="field-label">{{ t("profileId") }} <span class="help-dot" :title="t('profileIdHelp')" tabindex="0">?</span></span>
|
|
523
|
+
<select v-model="selectedConfigProfileId" @change="loadProfileForm" class="input">
|
|
524
|
+
<option v-for="p in aiConfig.profiles" :key="p.id" :value="p.id">{{ profileOptionLabel(p) }}</option>
|
|
525
|
+
</select>
|
|
526
|
+
</label>
|
|
527
|
+
<label class="grid gap-1 text-sm">{{ t("profileName") }}
|
|
528
|
+
<input v-model="profileForm.name" class="input" />
|
|
529
|
+
</label>
|
|
530
|
+
<label class="grid gap-1 text-sm md:col-span-2">
|
|
531
|
+
<span class="field-label">{{ t("implementation") }} <span class="help-dot" :title="t('implementationHelp')" tabindex="0">?</span></span>
|
|
532
|
+
<select v-model="profileForm.implementation" class="input font-mono text-xs">
|
|
533
|
+
<option v-for="adapter in adapters" :key="adapter.id" :value="adapter.id">{{ adapter.id }} - {{ adapter.name || adapter.id }}</option>
|
|
534
|
+
</select>
|
|
535
|
+
</label>
|
|
536
|
+
<label class="flex items-center gap-2 py-2 text-sm">
|
|
537
|
+
<input type="checkbox" v-model="profileForm.enabled" />
|
|
538
|
+
{{ t("enabled") }}
|
|
539
|
+
<span class="help-dot" :title="t('enabledHelp')" tabindex="0">?</span>
|
|
540
|
+
</label>
|
|
541
|
+
<label class="grid gap-1 text-sm md:col-span-2">{{ t("baseUrl") }}
|
|
542
|
+
<input v-model="profileForm.baseUrl" class="input font-mono text-xs" placeholder="https://api.anthropic.com" />
|
|
543
|
+
</label>
|
|
544
|
+
<label class="grid gap-1 text-sm">{{ t("apiKey") }}
|
|
545
|
+
<input v-model="profileForm.apiKey" type="password" class="input font-mono text-xs" autocomplete="off" />
|
|
546
|
+
</label>
|
|
547
|
+
<label class="grid gap-1 text-sm">{{ t("apiKeyEnv") }}
|
|
548
|
+
<input v-model="profileForm.apiKeyEnv" class="input font-mono text-xs" placeholder="ANTHROPIC_AUTH_TOKEN" />
|
|
549
|
+
</label>
|
|
550
|
+
<label class="grid gap-1 text-sm">{{ t("modelName") }}
|
|
551
|
+
<input v-model="profileForm.model" class="input font-mono text-xs" placeholder="claude-haiku-4-5" />
|
|
552
|
+
</label>
|
|
553
|
+
<label class="grid gap-1 text-sm">
|
|
554
|
+
<span class="field-label">{{ t("anthropicVersion") }} <span class="help-dot" :title="t('apiVersionHelp')" tabindex="0">?</span></span>
|
|
555
|
+
<input v-model="profileForm.version" class="input font-mono text-xs" placeholder="2023-06-01" />
|
|
556
|
+
</label>
|
|
557
|
+
<label class="grid gap-1 text-sm">
|
|
558
|
+
<span class="field-label">{{ t("temperature") }} <span class="help-dot" :title="t('temperatureHelp')" tabindex="0">?</span></span>
|
|
559
|
+
<input v-model.number="profileForm.temperature" type="number" min="0" max="2" step="0.1" class="input" />
|
|
560
|
+
</label>
|
|
561
|
+
<label class="grid gap-1 text-sm">
|
|
562
|
+
<span class="field-label">{{ t("maxTokens") }} <span class="help-dot" :title="t('maxTokensHelp')" tabindex="0">?</span></span>
|
|
563
|
+
<input v-model.number="profileForm.maxTokens" type="number" min="1" step="1" class="input" />
|
|
564
|
+
</label>
|
|
565
|
+
<label class="grid gap-1 text-sm">{{ t("timeoutMs") }}
|
|
566
|
+
<input v-model.number="profileForm.timeoutMs" type="number" min="1000" step="1000" class="input" />
|
|
567
|
+
</label>
|
|
568
|
+
</div>
|
|
569
|
+
<div class="mt-4 grid gap-3">
|
|
570
|
+
<label class="grid gap-1 text-sm">{{ t("testPrompt") }}
|
|
571
|
+
<input v-model="testPrompt" class="input" />
|
|
572
|
+
</label>
|
|
573
|
+
<div class="flex flex-wrap gap-2">
|
|
574
|
+
<button type="button" @click="testCurrentProfile" :disabled="testingProfile" class="btn-primary disabled:opacity-50">
|
|
575
|
+
{{ testingProfile ? t("testing") : t("testModel") }}
|
|
576
|
+
</button>
|
|
577
|
+
</div>
|
|
578
|
+
<pre v-if="testResult" class="max-h-56 overflow-auto rounded-xl bg-slate-950 p-4 text-xs text-slate-100 whitespace-pre-wrap">{{ testResult }}</pre>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
316
582
|
<label class="grid gap-1 text-sm">{{ t("rawProfiles") }}
|
|
317
|
-
<textarea v-model="aiProfilesText" rows="
|
|
583
|
+
<textarea v-model="aiProfilesText" rows="10" class="input font-mono text-xs"></textarea>
|
|
318
584
|
</label>
|
|
585
|
+
<div class="flex justify-end">
|
|
586
|
+
<button type="button" @click="saveAiProfiles" class="btn">{{ t("saveRawProfiles") }}</button>
|
|
587
|
+
</div>
|
|
319
588
|
<div v-if="aiMessage" class="rounded-lg border p-3 text-sm panel2">{{ aiMessage }}</div>
|
|
320
589
|
</div>
|
|
321
590
|
</div>
|
|
@@ -323,9 +592,10 @@
|
|
|
323
592
|
<h2 class="font-semibold">{{ t("projectProfile") }}</h2>
|
|
324
593
|
<div v-if="selectedProject" class="mt-4 grid gap-3">
|
|
325
594
|
<div class="text-sm muted">{{ selectedProject.displayName }}</div>
|
|
326
|
-
<label class="grid gap-1 text-sm">
|
|
595
|
+
<label class="grid gap-1 text-sm">
|
|
596
|
+
<span class="field-label">{{ t("defaultProfile") }} <span class="help-dot" :title="t('projectProfileHelp')" tabindex="0">?</span></span>
|
|
327
597
|
<select v-model="selectedAiProfileId" class="input">
|
|
328
|
-
<option v-for="p in aiConfig.profiles" :key="p.id" :value="p.id">{{ p
|
|
598
|
+
<option v-for="p in aiConfig.profiles" :key="p.id" :value="p.id" :disabled="p.enabled === false">{{ profileOptionLabel(p) }}</option>
|
|
329
599
|
</select>
|
|
330
600
|
</label>
|
|
331
601
|
<label class="grid gap-1 text-sm">{{ t("knowledgeLanguage") }}
|
|
@@ -338,6 +608,42 @@
|
|
|
338
608
|
<div class="text-xs muted">{{ t("availableAdapters") }}: {{ adapters.map(a => a.id).join(", ") || "-" }}</div>
|
|
339
609
|
</div>
|
|
340
610
|
</aside>
|
|
611
|
+
<div v-if="false && removeDialog.open" class="fixed inset-0 z-50 flex items-center justify-center bg-black/45 p-4">
|
|
612
|
+
<div class="panel w-full max-w-2xl rounded-xl border p-5 shadow-xl">
|
|
613
|
+
<div class="flex items-start justify-between gap-4">
|
|
614
|
+
<div>
|
|
615
|
+
<h2 class="text-lg font-semibold">{{ t("removeProject") }}</h2>
|
|
616
|
+
<div class="mt-1 text-sm muted">{{ removeDialog.preview.displayName || removeDialog.slug }} / {{ removeDialog.slug }}</div>
|
|
617
|
+
</div>
|
|
618
|
+
<button @click="closeRemoveProject" class="btn">×</button>
|
|
619
|
+
</div>
|
|
620
|
+
<div class="mt-4 grid gap-3 text-sm">
|
|
621
|
+
<div class="rounded-lg border p-3 panel2 break-all">
|
|
622
|
+
<div class="muted">{{ t("kbPath") }}</div>
|
|
623
|
+
<div class="mt-1 font-mono text-xs">{{ removeDialog.preview.kbPath || "-" }}</div>
|
|
624
|
+
<div class="mt-2 text-xs muted">{{ removeDialog.preview.fileCount || 0 }} {{ t("files") }} · {{ formatBytes(removeDialog.preview.kbSizeBytes || 0) }}</div>
|
|
625
|
+
</div>
|
|
626
|
+
<label class="flex items-center gap-2">
|
|
627
|
+
<input type="checkbox" v-model="removeDialog.deleteKb" />
|
|
628
|
+
<span>{{ t("deleteKbFiles") }}</span>
|
|
629
|
+
</label>
|
|
630
|
+
<label class="grid gap-1">
|
|
631
|
+
<span class="muted">{{ t("removeReason") }}</span>
|
|
632
|
+
<input v-model="removeDialog.reason" class="input" />
|
|
633
|
+
</label>
|
|
634
|
+
<label v-if="removeDialog.deleteKb" class="grid gap-1">
|
|
635
|
+
<span class="muted">{{ t("typeSlugToConfirm") }}</span>
|
|
636
|
+
<input v-model="removeDialog.confirmText" class="input font-mono" />
|
|
637
|
+
</label>
|
|
638
|
+
<div v-if="removeDialog.error" class="rounded-lg border border-red-500/25 bg-red-500/10 p-3 text-sm" style="color: var(--bad)">{{ removeDialog.error }}</div>
|
|
639
|
+
<div v-if="removeDialog.preview.hasRunningJobs" class="rounded-lg border border-amber-500/25 bg-amber-500/10 p-3 text-sm" style="color: var(--warn)">{{ t("projectHasRunningJobs") }}</div>
|
|
640
|
+
</div>
|
|
641
|
+
<div class="mt-5 flex justify-end gap-2">
|
|
642
|
+
<button @click="closeRemoveProject" class="btn">{{ t("cancel") }}</button>
|
|
643
|
+
<button @click="confirmRemoveProject" :disabled="removeProjectDisabled" class="btn-danger">{{ removeDialog.deleteKb ? t("deleteProjectAndKb") : t("unregisterProject") }}</button>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
341
647
|
</section>
|
|
342
648
|
|
|
343
649
|
<section v-else-if="activeView === 'runs'" class="grid gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
|
|
@@ -365,27 +671,34 @@
|
|
|
365
671
|
</div>
|
|
366
672
|
<div class="flex flex-wrap gap-2">
|
|
367
673
|
<button @click="loadRunDetail(selectedSlug, selectedRun.runId)" class="btn">{{ t("refreshRun") }}</button>
|
|
368
|
-
<button @click="applySelectedDrafts" class="btn-primary">{{
|
|
674
|
+
<button @click="applySelectedDrafts" class="btn-primary">{{ applyDraftButtonLabel }}</button>
|
|
369
675
|
<button @click="rejectRun" class="btn-danger">{{ t("rejectRun") }}</button>
|
|
370
676
|
</div>
|
|
371
677
|
</div>
|
|
372
678
|
|
|
373
|
-
<div class="mt-4 flex items-center gap-3 text-sm">
|
|
679
|
+
<div class="mt-4 flex flex-wrap items-center gap-3 text-sm">
|
|
374
680
|
<label class="flex items-center gap-2"><input type="checkbox" v-model="allowGoalEdit" /> {{ t("allowGoalEdit") }}</label>
|
|
375
|
-
<
|
|
681
|
+
<select v-model="draftBranchFilter" class="input py-2 text-sm">
|
|
682
|
+
<option value="">{{ t("allBranches") }}</option>
|
|
683
|
+
<option v-for="b in draftBranches" :key="b" :value="b">{{ b }}</option>
|
|
684
|
+
</select>
|
|
685
|
+
<span class="muted">{{ filteredDrafts.length }} / {{ drafts.length }} {{ t("draftFiles") }}</span>
|
|
376
686
|
</div>
|
|
377
687
|
|
|
378
688
|
<div class="mt-4 grid gap-4 xl:grid-cols-[320px_1fr]">
|
|
379
689
|
<div class="space-y-2">
|
|
380
|
-
<label v-for="d in
|
|
381
|
-
<span class="
|
|
690
|
+
<label v-for="d in filteredDrafts" :key="d.path" class="flex cursor-pointer items-center justify-between gap-3 rounded-lg border p-3 text-sm panel2">
|
|
691
|
+
<span class="min-w-0">
|
|
692
|
+
<span class="block truncate">{{ d.path }}</span>
|
|
693
|
+
<span class="chip mt-1">{{ d.sourceBranch || t("unknownBranch") }}</span>
|
|
694
|
+
</span>
|
|
382
695
|
<input type="checkbox" v-model="draftSelection[d.path]" />
|
|
383
696
|
</label>
|
|
384
697
|
</div>
|
|
385
698
|
<div>
|
|
386
699
|
<select v-model="previewDraftPath" @change="loadDraftPreview" class="input mb-3 w-full">
|
|
387
700
|
<option value="">{{ t("selectDraftPreview") }}</option>
|
|
388
|
-
<option v-for="d in
|
|
701
|
+
<option v-for="d in filteredDrafts" :key="d.path" :value="d.path">{{ d.path }}</option>
|
|
389
702
|
</select>
|
|
390
703
|
<pre class="max-h-[62vh] overflow-auto rounded-xl bg-slate-950 p-4 text-xs text-slate-100 scrollbar-thin whitespace-pre-wrap">{{ draftPreview || t("noPreview") }}</pre>
|
|
391
704
|
</div>
|
|
@@ -439,38 +752,184 @@
|
|
|
439
752
|
</div>
|
|
440
753
|
</section>
|
|
441
754
|
|
|
442
|
-
<section v-else-if="activeView === 'logs'" class="grid gap-5
|
|
443
|
-
<aside class="panel rounded-xl border p-5">
|
|
444
|
-
<div class="mb-3 flex items-center justify-between">
|
|
445
|
-
<h2 class="font-semibold">{{ t("jobHistory") }}</h2>
|
|
446
|
-
<button @click="loadJobs" class="btn">{{ t("reload") }}</button>
|
|
447
|
-
</div>
|
|
448
|
-
<div class="space-y-2">
|
|
449
|
-
<button v-for="job in jobHistory" :key="job.jobId" @click="loadJobDetail(job.jobId)"
|
|
450
|
-
class="w-full rounded-lg border p-3 text-left text-sm panel2">
|
|
451
|
-
<div class="font-medium">{{ job.mode }} / {{ job.status }}</div>
|
|
452
|
-
<div class="mt-1 truncate text-xs muted">{{ job.slug }} / {{ job.jobId }}</div>
|
|
453
|
-
</button>
|
|
454
|
-
</div>
|
|
455
|
-
</aside>
|
|
755
|
+
<section v-else-if="activeView === 'logs'" class="grid gap-5">
|
|
456
756
|
<section class="panel rounded-xl border p-5">
|
|
457
|
-
<div class="mb-4 flex items-
|
|
757
|
+
<div class="mb-4 flex flex-col gap-3 xl:flex-row xl:items-end xl:justify-between">
|
|
458
758
|
<div>
|
|
459
|
-
<h2 class="text-lg font-semibold">{{ t("
|
|
460
|
-
<div class="mt-1 text-sm muted">{{
|
|
759
|
+
<h2 class="text-lg font-semibold">{{ t("logs") }}</h2>
|
|
760
|
+
<div class="mt-1 text-sm muted">{{ loggingConfig.rootPath || "-" }}</div>
|
|
761
|
+
</div>
|
|
762
|
+
<div class="grid gap-2 md:grid-cols-5 xl:min-w-[760px]">
|
|
763
|
+
<input v-model="logFilters.dateFrom" type="date" class="input text-sm" />
|
|
764
|
+
<select v-model="logFilters.level" class="input text-sm">
|
|
765
|
+
<option value="all">{{ t("allLevels") }}</option>
|
|
766
|
+
<option value="info">info</option>
|
|
767
|
+
<option value="warn">warn</option>
|
|
768
|
+
<option value="error">error</option>
|
|
769
|
+
</select>
|
|
770
|
+
<select v-model="logFilters.projectSlug" class="input text-sm">
|
|
771
|
+
<option value="all">{{ t("allProjects") }}</option>
|
|
772
|
+
<option v-for="p in projectList" :key="p.slug" :value="p.slug">{{ p.slug }}</option>
|
|
773
|
+
</select>
|
|
774
|
+
<input v-model="logFilters.q" class="input text-sm" :placeholder="t('searchLogs')" />
|
|
775
|
+
<button @click="loadStructuredLogs" class="btn">{{ t("search") }}</button>
|
|
461
776
|
</div>
|
|
462
|
-
<button @click="refreshAll" class="btn">{{ t("refresh") }}</button>
|
|
463
777
|
</div>
|
|
464
|
-
<
|
|
778
|
+
<div class="overflow-auto rounded-lg border line">
|
|
779
|
+
<table class="w-full text-left text-sm">
|
|
780
|
+
<thead class="panel2 muted">
|
|
781
|
+
<tr>
|
|
782
|
+
<th class="px-3 py-2">{{ t("time") }}</th>
|
|
783
|
+
<th class="px-3 py-2">Level</th>
|
|
784
|
+
<th class="px-3 py-2">{{ t("project") }}</th>
|
|
785
|
+
<th class="px-3 py-2">Source</th>
|
|
786
|
+
<th class="px-3 py-2">{{ t("message") }}</th>
|
|
787
|
+
</tr>
|
|
788
|
+
</thead>
|
|
789
|
+
<tbody>
|
|
790
|
+
<tr v-for="log in structuredLogs" :key="log.ts + log.event + log.message" @click="selectedLog = log"
|
|
791
|
+
class="cursor-pointer border-t line hover:brightness-[0.98]">
|
|
792
|
+
<td class="px-3 py-2 whitespace-nowrap text-xs">{{ formatTime(log.ts) }}</td>
|
|
793
|
+
<td class="px-3 py-2"><span :class="['chip', log.level === 'error' ? 'text-red-500' : log.level === 'warn' ? 'text-amber-600' : '']">{{ log.level }}</span></td>
|
|
794
|
+
<td class="px-3 py-2">{{ log.projectSlug || "-" }}</td>
|
|
795
|
+
<td class="px-3 py-2">{{ log.source || "-" }}</td>
|
|
796
|
+
<td class="px-3 py-2">{{ log.message }}</td>
|
|
797
|
+
</tr>
|
|
798
|
+
</tbody>
|
|
799
|
+
</table>
|
|
800
|
+
</div>
|
|
801
|
+
<div v-if="!structuredLogs.length" class="mt-4 rounded-lg border p-3 text-sm panel2">{{ t("noOutput") }}</div>
|
|
465
802
|
</section>
|
|
803
|
+
<section v-if="selectedLog" class="panel rounded-xl border p-5">
|
|
804
|
+
<h3 class="font-semibold">{{ t("logDetail") }}</h3>
|
|
805
|
+
<pre class="mt-3 max-h-[42vh] overflow-auto rounded-xl bg-slate-950 p-4 text-xs text-slate-100 scrollbar-thin whitespace-pre-wrap">{{ JSON.stringify(selectedLog, null, 2) }}</pre>
|
|
806
|
+
</section>
|
|
807
|
+
</section>
|
|
808
|
+
|
|
809
|
+
<section v-else-if="activeView === 'settings'" class="grid gap-5 xl:grid-cols-2">
|
|
810
|
+
<div class="panel rounded-xl border p-5">
|
|
811
|
+
<h2 class="text-lg font-semibold">{{ t("knowledgeStoreSettings") }}</h2>
|
|
812
|
+
<div class="mt-4 grid gap-3">
|
|
813
|
+
<label class="grid gap-1 text-sm">{{ t("knowledgeStoreRoot") }}
|
|
814
|
+
<input v-model="knowledgeStoreConfig.rootPath" class="input font-mono text-xs" />
|
|
815
|
+
</label>
|
|
816
|
+
<label class="grid gap-1 text-sm">{{ t("remote") }}
|
|
817
|
+
<input v-model="knowledgeStoreConfig.git.remoteUrl" class="input font-mono text-xs" />
|
|
818
|
+
</label>
|
|
819
|
+
<label class="grid gap-1 text-sm">{{ t("branch") }}
|
|
820
|
+
<input v-model="knowledgeStoreConfig.git.branch" class="input font-mono text-xs" />
|
|
821
|
+
</label>
|
|
822
|
+
<div class="flex flex-wrap gap-2">
|
|
823
|
+
<button @click="saveKnowledgeStoreConfig" class="btn-primary">{{ t("save") }}</button>
|
|
824
|
+
<button @click="previewKnowledgeMigration" class="btn">{{ t("previewMigration") }}</button>
|
|
825
|
+
<button @click="executeKnowledgeMigration" class="btn">{{ t("executeMigration") }}</button>
|
|
826
|
+
</div>
|
|
827
|
+
<pre v-if="knowledgeMigrationPlan.length" class="max-h-64 overflow-auto rounded-xl bg-slate-950 p-3 text-xs text-slate-100">{{ JSON.stringify(knowledgeMigrationPlan, null, 2) }}</pre>
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
<div class="panel rounded-xl border p-5">
|
|
831
|
+
<h2 class="text-lg font-semibold">{{ t("loggingSettings") }}</h2>
|
|
832
|
+
<div class="mt-4 grid gap-3">
|
|
833
|
+
<label class="grid gap-1 text-sm">{{ t("logDirectory") }}
|
|
834
|
+
<input v-model="loggingConfig.rootPath" class="input font-mono text-xs" />
|
|
835
|
+
</label>
|
|
836
|
+
<label class="grid gap-1 text-sm">{{ t("retentionDays") }}
|
|
837
|
+
<input v-model.number="loggingConfig.retentionDays" type="number" min="1" class="input" />
|
|
838
|
+
</label>
|
|
839
|
+
<div class="flex flex-wrap gap-3 text-sm">
|
|
840
|
+
<label class="flex items-center gap-2"><input type="checkbox" value="info" v-model="loggingConfig.levels" /> info</label>
|
|
841
|
+
<label class="flex items-center gap-2"><input type="checkbox" value="warn" v-model="loggingConfig.levels" /> warn</label>
|
|
842
|
+
<label class="flex items-center gap-2"><input type="checkbox" value="error" v-model="loggingConfig.levels" /> error</label>
|
|
843
|
+
</div>
|
|
844
|
+
<button @click="saveLoggingConfig" class="btn-primary">{{ t("save") }}</button>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
</section>
|
|
848
|
+
|
|
849
|
+
<section v-else-if="activeView === 'prompts'" class="grid gap-5">
|
|
850
|
+
<div class="panel rounded-xl border p-5">
|
|
851
|
+
<div class="mb-3 flex items-center justify-between">
|
|
852
|
+
<h2 class="text-lg font-semibold">{{ t("promptsConfig") }}</h2>
|
|
853
|
+
<div class="flex gap-2">
|
|
854
|
+
<button @click="loadPrompts" class="btn">{{ t("reload") }}</button>
|
|
855
|
+
<button @click="savePrompts" :disabled="promptsSaving" class="btn-primary">{{ promptsSaving ? t("saving") : t("save") }}</button>
|
|
856
|
+
<button @click="activeView = 'dashboard'" class="btn">{{ t("back") }}</button>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
<p class="text-sm muted mb-4">Each entry controls how Claude is prompted for a step (initial analysis, commit batch, etc.). Use <code v-pre class="font-mono text-xs">{{SLUG}}</code>, <code v-pre class="font-mono text-xs">{{PROJECT_PATH}}</code>, <code v-pre class="font-mono text-xs">{{PRIMARY_LANGUAGE}}</code>, <code v-pre class="font-mono text-xs">{{KNOWLEDGE_LANGUAGE}}</code> placeholders.</p>
|
|
860
|
+
<div v-if="promptsError" class="mb-3 rounded-lg border px-3 py-2 text-sm" style="border-color: var(--bad); color: var(--bad)">{{ promptsError }}</div>
|
|
861
|
+
<div v-if="promptsSaved" class="mb-3 rounded-lg border px-3 py-2 text-sm" style="border-color: var(--good); color: var(--good)">saved.</div>
|
|
862
|
+
<div v-if="promptsConfig" class="space-y-5">
|
|
863
|
+
<div v-for="key in Object.keys(promptsConfig.prompts || {})" :key="key" class="rounded-lg border p-4 panel2">
|
|
864
|
+
<div class="mb-2 font-mono text-sm font-semibold">{{ key }}</div>
|
|
865
|
+
<div class="grid gap-2">
|
|
866
|
+
<label class="grid gap-1 text-xs">description
|
|
867
|
+
<input v-model="promptsConfig.prompts[key].description" class="input font-mono text-xs" />
|
|
868
|
+
</label>
|
|
869
|
+
<label class="grid gap-1 text-xs">model
|
|
870
|
+
<input v-model="promptsConfig.prompts[key].model" class="input font-mono text-xs" placeholder="sonnet" />
|
|
871
|
+
</label>
|
|
872
|
+
<label class="grid gap-1 text-xs">allowedTools (comma-separated)
|
|
873
|
+
<input :value="(promptsConfig.prompts[key].allowedTools || []).join(', ')"
|
|
874
|
+
@input="promptsConfig.prompts[key].allowedTools = $event.target.value.split(',').map(s=>s.trim()).filter(Boolean)"
|
|
875
|
+
class="input font-mono text-xs" />
|
|
876
|
+
</label>
|
|
877
|
+
<label class="grid gap-1 text-xs">systemPrompt
|
|
878
|
+
<textarea v-model="promptsConfig.prompts[key].systemPrompt" rows="6" class="input font-mono text-xs"></textarea>
|
|
879
|
+
</label>
|
|
880
|
+
<label class="grid gap-1 text-xs">userPrompt
|
|
881
|
+
<textarea v-model="promptsConfig.prompts[key].userPrompt" rows="8" class="input font-mono text-xs"></textarea>
|
|
882
|
+
</label>
|
|
883
|
+
</div>
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
<div v-else class="text-sm muted">loading…</div>
|
|
887
|
+
</div>
|
|
466
888
|
</section>
|
|
889
|
+
|
|
890
|
+
<div v-if="removeDialog.open" class="fixed inset-0 z-50 flex items-center justify-center bg-black/45 p-4">
|
|
891
|
+
<div class="panel w-full max-w-2xl rounded-xl border p-5 shadow-xl">
|
|
892
|
+
<div class="flex items-start justify-between gap-4">
|
|
893
|
+
<div>
|
|
894
|
+
<h2 class="text-lg font-semibold">{{ t("removeProject") }}</h2>
|
|
895
|
+
<div class="mt-1 text-sm muted">{{ removeDialog.preview.displayName || removeDialog.slug }} / {{ removeDialog.slug }}</div>
|
|
896
|
+
</div>
|
|
897
|
+
<button @click="closeRemoveProject" class="btn">×</button>
|
|
898
|
+
</div>
|
|
899
|
+
<div class="mt-4 grid gap-3 text-sm">
|
|
900
|
+
<div class="rounded-lg border p-3 panel2 break-all">
|
|
901
|
+
<div class="muted">{{ t("kbPath") }}</div>
|
|
902
|
+
<div class="mt-1 font-mono text-xs">{{ removeDialog.preview.kbPath || "-" }}</div>
|
|
903
|
+
<div class="mt-2 text-xs muted">{{ removeDialog.preview.fileCount || 0 }} {{ t("files") }} · {{ formatBytes(removeDialog.preview.kbSizeBytes || 0) }}</div>
|
|
904
|
+
</div>
|
|
905
|
+
<label class="flex items-center gap-2">
|
|
906
|
+
<input type="checkbox" v-model="removeDialog.deleteKb" />
|
|
907
|
+
<span>{{ t("deleteKbFiles") }}</span>
|
|
908
|
+
</label>
|
|
909
|
+
<label class="grid gap-1">
|
|
910
|
+
<span class="muted">{{ t("removeReason") }}</span>
|
|
911
|
+
<input v-model="removeDialog.reason" class="input" />
|
|
912
|
+
</label>
|
|
913
|
+
<label v-if="removeDialog.deleteKb" class="grid gap-1">
|
|
914
|
+
<span class="muted">{{ t("typeSlugToConfirm") }}</span>
|
|
915
|
+
<input v-model="removeDialog.confirmText" class="input font-mono" />
|
|
916
|
+
</label>
|
|
917
|
+
<div v-if="removeDialog.error" class="rounded-lg border border-red-500/25 bg-red-500/10 p-3 text-sm" style="color: var(--bad)">{{ removeDialog.error }}</div>
|
|
918
|
+
<div v-if="removeDialog.preview.hasRunningJobs" class="rounded-lg border border-amber-500/25 bg-amber-500/10 p-3 text-sm" style="color: var(--warn)">{{ t("projectHasRunningJobs") }}</div>
|
|
919
|
+
</div>
|
|
920
|
+
<div class="mt-5 flex justify-end gap-2">
|
|
921
|
+
<button @click="closeRemoveProject" class="btn">{{ t("cancel") }}</button>
|
|
922
|
+
<button @click="confirmRemoveProject" :disabled="removeProjectDisabled" class="btn-danger disabled:opacity-50">{{ removeDialog.deleteKb ? t("deleteProjectAndKb") : t("unregisterProject") }}</button>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
467
926
|
</main>
|
|
468
|
-
</div>
|
|
469
927
|
</div>
|
|
470
928
|
</div>
|
|
929
|
+
</div>
|
|
471
930
|
|
|
472
931
|
<script>
|
|
473
|
-
const { createApp, reactive, ref, computed, onMounted, onUnmounted, watch } = Vue;
|
|
932
|
+
const { createApp, reactive, ref, computed, onMounted, onUnmounted, watch, nextTick } = Vue;
|
|
474
933
|
|
|
475
934
|
const I18N = {
|
|
476
935
|
en: {
|
|
@@ -484,6 +943,7 @@ const I18N = {
|
|
|
484
943
|
runsDrafts: "Runs / Drafts",
|
|
485
944
|
schedule: "Schedule",
|
|
486
945
|
logs: "Logs",
|
|
946
|
+
settings: "Settings",
|
|
487
947
|
consoleSubtitle: "Backend-driven project supervision console",
|
|
488
948
|
refreshing: "refreshing",
|
|
489
949
|
refresh: "Refresh",
|
|
@@ -495,6 +955,13 @@ const I18N = {
|
|
|
495
955
|
validateGit: "Validate Git",
|
|
496
956
|
scan: "Scan",
|
|
497
957
|
safeAnalyze: "Safe Analyze",
|
|
958
|
+
runKnowledgeUpdate: "Run Knowledge Update",
|
|
959
|
+
editProjectGoal: "Edit Project Goal",
|
|
960
|
+
showAdvanced: "Show advanced diagnostics",
|
|
961
|
+
hideAdvanced: "Hide advanced diagnostics",
|
|
962
|
+
knowledgeUpdateRunning: "Knowledge update is running...",
|
|
963
|
+
knowledgeUpdateDone: "Knowledge update completed.",
|
|
964
|
+
knowledgeUpdateReviewRequired: "Review required",
|
|
498
965
|
repo: "Repo",
|
|
499
966
|
pending: "Pending",
|
|
500
967
|
goal: "Goal",
|
|
@@ -502,6 +969,17 @@ const I18N = {
|
|
|
502
969
|
projectOperations: "Project Operations",
|
|
503
970
|
initKb: "Init KB",
|
|
504
971
|
migrateV2: "Migrate v2",
|
|
972
|
+
migrateV3: "Migrate v3",
|
|
973
|
+
removeProject: "Remove Project",
|
|
974
|
+
unregisterProject: "Unregister Project",
|
|
975
|
+
deleteProjectAndKb: "Delete Project and KB",
|
|
976
|
+
deleteKbFiles: "Also delete KB files",
|
|
977
|
+
removeReason: "Reason",
|
|
978
|
+
typeSlugToConfirm: "Type the project slug to confirm deletion",
|
|
979
|
+
projectHasRunningJobs: "This project has a running job and cannot be removed now.",
|
|
980
|
+
kbPath: "KB path",
|
|
981
|
+
files: "files",
|
|
982
|
+
cancel: "Cancel",
|
|
505
983
|
runScanJob: "Run scan job",
|
|
506
984
|
initialAnalysis: "Initial analysis",
|
|
507
985
|
commitAnalysis: "Commit analysis",
|
|
@@ -525,6 +1003,42 @@ const I18N = {
|
|
|
525
1003
|
runningJobs: "Running Jobs",
|
|
526
1004
|
noRunningJob: "No running job.",
|
|
527
1005
|
lastResult: "Last Result",
|
|
1006
|
+
claudeTerminal: "Claude Terminal",
|
|
1007
|
+
claudeWorkbench: "Claude Workbench",
|
|
1008
|
+
restoreSession: "Restore",
|
|
1009
|
+
continuity: "Context",
|
|
1010
|
+
permissionStatus: "Permission",
|
|
1011
|
+
recoveryStatus: "Recovery",
|
|
1012
|
+
pendingReview: "pending",
|
|
1013
|
+
approvedStatus: "ready",
|
|
1014
|
+
recoverable: "available",
|
|
1015
|
+
permissionRequired: "Permission required",
|
|
1016
|
+
approve: "Allow",
|
|
1017
|
+
deny: "Deny",
|
|
1018
|
+
permissionApproved: "Permission approved",
|
|
1019
|
+
permissionDenied: "Permission denied",
|
|
1020
|
+
permissionTurn: "Turn",
|
|
1021
|
+
permissionMode: "Mode",
|
|
1022
|
+
permissionTools: "Tools",
|
|
1023
|
+
permissionCwd: "Working dir",
|
|
1024
|
+
terminalNeedPermission: "Waiting for permission approval.",
|
|
1025
|
+
sessionRecovered: "session restored",
|
|
1026
|
+
noRestorableSession: "No previous Claude session for this project.",
|
|
1027
|
+
analyze: "Analyze",
|
|
1028
|
+
stop: "Stop",
|
|
1029
|
+
clear: "Clear",
|
|
1030
|
+
editPrompts: "Edit Prompts",
|
|
1031
|
+
send: "Send",
|
|
1032
|
+
claudeHasWriteAccess: "Claude has read/write access to this project's source tree.",
|
|
1033
|
+
terminalEmpty: "Click Analyze to start a Claude Code session for the selected project.",
|
|
1034
|
+
terminalNeedSession: "Start an analysis first.",
|
|
1035
|
+
terminalWaitInit: "Waiting for Claude session to initialize…",
|
|
1036
|
+
terminalBusy: "Claude is running, wait for the current turn to finish.",
|
|
1037
|
+
terminalInputPlaceholder: "Type follow-up prompt, then press Enter…",
|
|
1038
|
+
thinking: "Thinking...",
|
|
1039
|
+
toolRunning: "running",
|
|
1040
|
+
systemPromptLoaded: "System prompt loaded.",
|
|
1041
|
+
promptsConfig: "Prompt Config",
|
|
528
1042
|
saving: "Saving",
|
|
529
1043
|
save: "Save",
|
|
530
1044
|
reset: "Reset",
|
|
@@ -538,13 +1052,56 @@ const I18N = {
|
|
|
538
1052
|
initKbDirs: "Init KB directories",
|
|
539
1053
|
importHelp: "The backend will persist the project, initialize KB files, and auto-validate Git on upsert.",
|
|
540
1054
|
importTarget: "Import target",
|
|
1055
|
+
gitImportPreflight: "Git preflight",
|
|
1056
|
+
gitImportPreflightHelp: "Check whether the selected path is already a Git repository before import.",
|
|
1057
|
+
check: "Check",
|
|
1058
|
+
needsGitInit: "This path is not a Git repository. You can initialize local Git during import.",
|
|
1059
|
+
initLocalGit: "Initialize local Git repository",
|
|
1060
|
+
createInitialCommit: "Create initial commit",
|
|
1061
|
+
existingRemoteUrl: "Existing remote URL",
|
|
541
1062
|
kbPath: "KB path",
|
|
542
1063
|
schemaNormalized: "Schema fields are normalized by the backend.",
|
|
543
1064
|
gitPopulate: "Git validation will populate repoStatus, branch, remote and HEAD commit.",
|
|
544
1065
|
backedByAi: "Backed by /api/ai-profiles and project settings.",
|
|
545
1066
|
defaultProfile: "Default profile",
|
|
1067
|
+
defaultProfileHelp: "Fallback AI profile for new projects or projects without an explicit selection. Disabled profiles cannot be default.",
|
|
546
1068
|
rawProfiles: "Raw ai-profiles.json",
|
|
1069
|
+
profileEditor: "Profile editor",
|
|
1070
|
+
profileId: "Profile",
|
|
1071
|
+
profileIdHelp: "Choose which saved AI profile to edit.",
|
|
1072
|
+
profileName: "Profile name",
|
|
1073
|
+
enabled: "Enabled",
|
|
1074
|
+
enabledStatus: "enabled",
|
|
1075
|
+
disabledStatus: "disabled",
|
|
1076
|
+
enabledHelp: "Disabled profiles can be edited and tested, but cannot run project analyses.",
|
|
1077
|
+
defaultProfileDisabledError: "Default profile cannot be disabled.",
|
|
1078
|
+
profileIdPrompt: "Profile ID, for example minimax-m3, glm-4, or gpt-4.1",
|
|
1079
|
+
duplicateProfileError: "Profile ID already exists.",
|
|
1080
|
+
deleteDefaultProfileError: "Default profile cannot be deleted.",
|
|
1081
|
+
deleteProfileConfirm: "Delete this AI profile?",
|
|
1082
|
+
implementation: "Implementation",
|
|
1083
|
+
implementationHelp: "Backend adapter type. claude-code-agent calls an Anthropic-compatible Messages API.",
|
|
1084
|
+
baseUrl: "Base URL",
|
|
1085
|
+
apiKey: "API key",
|
|
1086
|
+
apiKeyEnv: "API key env",
|
|
1087
|
+
modelName: "Model name",
|
|
1088
|
+
anthropicVersion: "API version",
|
|
1089
|
+
apiVersionHelp: "Value sent as the anthropic-version header. Keep 2023-06-01 for most Anthropic-compatible endpoints.",
|
|
1090
|
+
temperature: "Temperature",
|
|
1091
|
+
temperatureHelp: "Sampling randomness. 0 is steadier; higher values are more creative but less stable.",
|
|
1092
|
+
maxTokens: "Max tokens",
|
|
1093
|
+
maxTokensHelp: "Maximum tokens the model may generate in one response.",
|
|
1094
|
+
timeoutMs: "Timeout (ms)",
|
|
1095
|
+
saveProfileForm: "Save profile",
|
|
1096
|
+
addProfile: "Add profile",
|
|
1097
|
+
deleteProfile: "Delete profile",
|
|
1098
|
+
saveRawProfiles: "Save raw JSON",
|
|
1099
|
+
testModel: "Test model",
|
|
1100
|
+
testPrompt: "Test prompt",
|
|
1101
|
+
testResult: "Test result",
|
|
1102
|
+
testing: "Testing",
|
|
547
1103
|
projectProfile: "Project profile",
|
|
1104
|
+
projectProfileHelp: "AI profile used by this selected project for initial and commit analysis.",
|
|
548
1105
|
assignProject: "Save project settings",
|
|
549
1106
|
availableAdapters: "Available adapters",
|
|
550
1107
|
knowledgeLanguage: "Knowledge output language",
|
|
@@ -552,11 +1109,14 @@ const I18N = {
|
|
|
552
1109
|
chinese: "Chinese",
|
|
553
1110
|
english: "English",
|
|
554
1111
|
reload: "Reload",
|
|
1112
|
+
back: "Back",
|
|
555
1113
|
selectRun: "Select a run to review drafts.",
|
|
556
1114
|
refreshRun: "Refresh run",
|
|
557
1115
|
applySelected: "Apply selected",
|
|
558
1116
|
rejectRun: "Reject run",
|
|
559
|
-
allowGoalEdit: "allow
|
|
1117
|
+
allowGoalEdit: "allow GOAL / architecture edits",
|
|
1118
|
+
allBranches: "All branches",
|
|
1119
|
+
unknownBranch: "unknown",
|
|
560
1120
|
draftFiles: "draft files",
|
|
561
1121
|
selectDraftPreview: "Select draft preview",
|
|
562
1122
|
noPreview: "(no preview)",
|
|
@@ -582,6 +1142,25 @@ const I18N = {
|
|
|
582
1142
|
failedOrPartialJobs: "failed or partial jobs",
|
|
583
1143
|
recentIssues: "Recent issues",
|
|
584
1144
|
pendingCommits: "Pending commits",
|
|
1145
|
+
pendingCommitDetails: "Pending commit details",
|
|
1146
|
+
pendingCommitDetailsHelp: "Commits waiting to be analyzed into the project knowledge base.",
|
|
1147
|
+
issueCenter: "Issue center",
|
|
1148
|
+
issueCenterHelp: "Warnings and errors that may require manual intervention.",
|
|
1149
|
+
subject: "Subject",
|
|
1150
|
+
project: "Project",
|
|
1151
|
+
message: "Message",
|
|
1152
|
+
allLevels: "All levels",
|
|
1153
|
+
allProjects: "All projects",
|
|
1154
|
+
searchLogs: "Search logs",
|
|
1155
|
+
search: "Search",
|
|
1156
|
+
logDetail: "Log detail",
|
|
1157
|
+
knowledgeStoreSettings: "Knowledge Store",
|
|
1158
|
+
knowledgeStoreRoot: "Knowledge store root",
|
|
1159
|
+
loggingSettings: "Logging",
|
|
1160
|
+
logDirectory: "Log directory",
|
|
1161
|
+
retentionDays: "Retention days",
|
|
1162
|
+
previewMigration: "Preview migration",
|
|
1163
|
+
executeMigration: "Execute migration",
|
|
585
1164
|
},
|
|
586
1165
|
zh: {
|
|
587
1166
|
appName: "AI 知识库",
|
|
@@ -612,6 +1191,16 @@ const I18N = {
|
|
|
612
1191
|
projectOperations: "项目操作",
|
|
613
1192
|
initKb: "初始化知识库",
|
|
614
1193
|
migrateV2: "迁移 v2",
|
|
1194
|
+
migrateV3: "迁移 v3",
|
|
1195
|
+
removeProject: "移除项目",
|
|
1196
|
+
unregisterProject: "仅移除项目",
|
|
1197
|
+
deleteProjectAndKb: "删除项目和知识库",
|
|
1198
|
+
deleteKbFiles: "同时删除知识库文件",
|
|
1199
|
+
removeReason: "移除原因",
|
|
1200
|
+
typeSlugToConfirm: "输入项目标识确认删除",
|
|
1201
|
+
projectHasRunningJobs: "当前项目有运行中的任务,暂时不能移除。",
|
|
1202
|
+
files: "个文件",
|
|
1203
|
+
cancel: "取消",
|
|
615
1204
|
runScanJob: "运行扫描任务",
|
|
616
1205
|
initialAnalysis: "整体分析",
|
|
617
1206
|
commitAnalysis: "提交分析",
|
|
@@ -635,6 +1224,42 @@ const I18N = {
|
|
|
635
1224
|
runningJobs: "运行中的任务",
|
|
636
1225
|
noRunningJob: "当前没有运行中的任务。",
|
|
637
1226
|
lastResult: "最近结果",
|
|
1227
|
+
claudeTerminal: "Claude 终端",
|
|
1228
|
+
claudeWorkbench: "Claude 工作台",
|
|
1229
|
+
restoreSession: "恢复",
|
|
1230
|
+
continuity: "上下文",
|
|
1231
|
+
permissionStatus: "权限",
|
|
1232
|
+
recoveryStatus: "恢复",
|
|
1233
|
+
pendingReview: "待确认",
|
|
1234
|
+
approvedStatus: "就绪",
|
|
1235
|
+
recoverable: "可恢复",
|
|
1236
|
+
permissionRequired: "需要授权",
|
|
1237
|
+
approve: "允许",
|
|
1238
|
+
deny: "拒绝",
|
|
1239
|
+
permissionApproved: "已允许执行",
|
|
1240
|
+
permissionDenied: "已拒绝执行",
|
|
1241
|
+
permissionTurn: "轮次",
|
|
1242
|
+
permissionMode: "模式",
|
|
1243
|
+
permissionTools: "工具",
|
|
1244
|
+
permissionCwd: "工作目录",
|
|
1245
|
+
terminalNeedPermission: "等待权限确认。",
|
|
1246
|
+
sessionRecovered: "会话已恢复",
|
|
1247
|
+
noRestorableSession: "当前项目没有可恢复的 Claude 会话。",
|
|
1248
|
+
analyze: "分析",
|
|
1249
|
+
stop: "停止",
|
|
1250
|
+
clear: "清屏",
|
|
1251
|
+
editPrompts: "编辑提示词",
|
|
1252
|
+
send: "发送",
|
|
1253
|
+
claudeHasWriteAccess: "Claude 对当前项目源码有读写权限。",
|
|
1254
|
+
terminalEmpty: "点击「分析」为选中项目启动一个 Claude Code 会话。",
|
|
1255
|
+
terminalNeedSession: "请先开始分析。",
|
|
1256
|
+
terminalWaitInit: "等待 Claude 会话初始化…",
|
|
1257
|
+
terminalBusy: "Claude 正在运行,请等待当前轮次结束。",
|
|
1258
|
+
terminalInputPlaceholder: "输入补充提示词,回车发送…",
|
|
1259
|
+
thinking: "思考中...",
|
|
1260
|
+
toolRunning: "运行中",
|
|
1261
|
+
systemPromptLoaded: "系统提示词已加载。",
|
|
1262
|
+
promptsConfig: "提示词配置",
|
|
638
1263
|
saving: "保存中",
|
|
639
1264
|
save: "保存",
|
|
640
1265
|
reset: "重置",
|
|
@@ -653,8 +1278,44 @@ const I18N = {
|
|
|
653
1278
|
gitPopulate: "Git 校验会写入 repoStatus、分支、远端和 HEAD commit。",
|
|
654
1279
|
backedByAi: "由 /api/ai-profiles 和项目级设置驱动。",
|
|
655
1280
|
defaultProfile: "默认模型配置",
|
|
1281
|
+
defaultProfileHelp: "新项目或未单独指定模型的项目会使用它。禁用的配置不能设为默认。",
|
|
656
1282
|
rawProfiles: "原始 ai-profiles.json",
|
|
1283
|
+
profileEditor: "模型配置编辑",
|
|
1284
|
+
profileId: "配置项",
|
|
1285
|
+
profileIdHelp: "选择当前要编辑的那一条模型配置。",
|
|
1286
|
+
profileName: "配置名称",
|
|
1287
|
+
enabled: "启用",
|
|
1288
|
+
enabledStatus: "已启用",
|
|
1289
|
+
disabledStatus: "已禁用",
|
|
1290
|
+
enabledHelp: "禁用后仍可编辑和测试,但不能用于项目整体分析或提交分析。",
|
|
1291
|
+
defaultProfileDisabledError: "默认模型配置不能是禁用状态。",
|
|
1292
|
+
profileIdPrompt: "配置 ID,例如 minimax-m3、glm-4、gpt-4.1",
|
|
1293
|
+
duplicateProfileError: "配置 ID 已存在。",
|
|
1294
|
+
deleteDefaultProfileError: "默认模型配置不能删除。",
|
|
1295
|
+
deleteProfileConfirm: "确定删除这个 AI 配置吗?",
|
|
1296
|
+
implementation: "实现类型",
|
|
1297
|
+
implementationHelp: "后端适配器类型。claude-code-agent 表示调用 Anthropic-compatible Messages API。",
|
|
1298
|
+
baseUrl: "Base 地址",
|
|
1299
|
+
apiKey: "API Key",
|
|
1300
|
+
apiKeyEnv: "API Key 环境变量",
|
|
1301
|
+
modelName: "模型名称",
|
|
1302
|
+
anthropicVersion: "API 版本",
|
|
1303
|
+
apiVersionHelp: "作为 anthropic-version 请求头发送。大多数兼容接口保持 2023-06-01 即可。",
|
|
1304
|
+
temperature: "Temperature",
|
|
1305
|
+
temperatureHelp: "控制输出随机性。0 更稳定,数值越高越发散。",
|
|
1306
|
+
maxTokens: "最大 tokens",
|
|
1307
|
+
maxTokensHelp: "限制模型单次最多生成多少 token。",
|
|
1308
|
+
timeoutMs: "超时时间(毫秒)",
|
|
1309
|
+
saveProfileForm: "保存模型配置",
|
|
1310
|
+
addProfile: "新增配置",
|
|
1311
|
+
deleteProfile: "删除配置",
|
|
1312
|
+
saveRawProfiles: "保存原始 JSON",
|
|
1313
|
+
testModel: "测试模型",
|
|
1314
|
+
testPrompt: "测试提示词",
|
|
1315
|
+
testResult: "测试结果",
|
|
1316
|
+
testing: "测试中",
|
|
657
1317
|
projectProfile: "项目模型配置",
|
|
1318
|
+
projectProfileHelp: "当前选中项目执行整体分析、提交分析时使用的模型配置。",
|
|
658
1319
|
assignProject: "保存项目设置",
|
|
659
1320
|
availableAdapters: "可用适配器",
|
|
660
1321
|
knowledgeLanguage: "知识库输出语言",
|
|
@@ -662,11 +1323,14 @@ const I18N = {
|
|
|
662
1323
|
chinese: "中文",
|
|
663
1324
|
english: "英文",
|
|
664
1325
|
reload: "重新加载",
|
|
1326
|
+
back: "返回",
|
|
665
1327
|
selectRun: "选择一次运行来审核草稿。",
|
|
666
1328
|
refreshRun: "刷新运行",
|
|
667
1329
|
applySelected: "应用选中草稿",
|
|
668
1330
|
rejectRun: "拒绝运行",
|
|
669
|
-
allowGoalEdit: "允许修改
|
|
1331
|
+
allowGoalEdit: "允许修改 GOAL / 架构",
|
|
1332
|
+
allBranches: "全部分支",
|
|
1333
|
+
unknownBranch: "未知分支",
|
|
670
1334
|
draftFiles: "个草稿文件",
|
|
671
1335
|
selectDraftPreview: "选择草稿预览",
|
|
672
1336
|
noPreview: "(暂无预览)",
|
|
@@ -695,6 +1359,43 @@ const I18N = {
|
|
|
695
1359
|
},
|
|
696
1360
|
};
|
|
697
1361
|
|
|
1362
|
+
Object.assign(I18N.zh, {
|
|
1363
|
+
settings: "设置",
|
|
1364
|
+
pendingCommitDetails: "待分析提交明细",
|
|
1365
|
+
pendingCommitDetailsHelp: "等待被分析并写入项目知识库的 Git 提交。",
|
|
1366
|
+
issueCenter: "异常中心",
|
|
1367
|
+
issueCenterHelp: "可能需要人工介入的警告和错误。",
|
|
1368
|
+
subject: "主题",
|
|
1369
|
+
project: "项目",
|
|
1370
|
+
message: "消息",
|
|
1371
|
+
allLevels: "全部级别",
|
|
1372
|
+
allProjects: "全部项目",
|
|
1373
|
+
searchLogs: "搜索日志",
|
|
1374
|
+
search: "搜索",
|
|
1375
|
+
logDetail: "日志详情",
|
|
1376
|
+
knowledgeStoreSettings: "知识库存储",
|
|
1377
|
+
knowledgeStoreRoot: "知识库根目录",
|
|
1378
|
+
loggingSettings: "日志设置",
|
|
1379
|
+
logDirectory: "日志目录",
|
|
1380
|
+
retentionDays: "保留天数",
|
|
1381
|
+
previewMigration: "预览迁移",
|
|
1382
|
+
executeMigration: "执行迁移",
|
|
1383
|
+
runKnowledgeUpdate: "运行知识库更新",
|
|
1384
|
+
editProjectGoal: "编辑项目目标",
|
|
1385
|
+
showAdvanced: "显示高级诊断",
|
|
1386
|
+
hideAdvanced: "隐藏高级诊断",
|
|
1387
|
+
knowledgeUpdateRunning: "知识库更新正在运行...",
|
|
1388
|
+
knowledgeUpdateDone: "知识库更新完成。",
|
|
1389
|
+
knowledgeUpdateReviewRequired: "需要人工审核",
|
|
1390
|
+
gitImportPreflight: "Git 导入预检",
|
|
1391
|
+
gitImportPreflightHelp: "导入前检查所选路径是否已经是 Git 仓库。",
|
|
1392
|
+
check: "检查",
|
|
1393
|
+
needsGitInit: "该路径还不是 Git 仓库,可以在导入时初始化本地 Git。",
|
|
1394
|
+
initLocalGit: "初始化本地 Git 仓库",
|
|
1395
|
+
createInitialCommit: "创建初始提交",
|
|
1396
|
+
existingRemoteUrl: "已有远程仓库地址",
|
|
1397
|
+
});
|
|
1398
|
+
|
|
698
1399
|
const StatusCard = {
|
|
699
1400
|
props: ["label", "value", "status"],
|
|
700
1401
|
template: `
|
|
@@ -718,6 +1419,15 @@ createApp({
|
|
|
718
1419
|
const runningJobs = ref([]);
|
|
719
1420
|
const jobHistory = ref([]);
|
|
720
1421
|
const selectedJob = ref(null);
|
|
1422
|
+
const summaryPanel = ref("");
|
|
1423
|
+
const pendingCommitItems = ref([]);
|
|
1424
|
+
const supervisionIssues = ref([]);
|
|
1425
|
+
const structuredLogs = ref([]);
|
|
1426
|
+
const selectedLog = ref(null);
|
|
1427
|
+
const logFilters = reactive({ dateFrom: new Date().toISOString().slice(0, 10), level: "all", projectSlug: "all", source: "all", q: "" });
|
|
1428
|
+
const knowledgeStoreConfig = reactive({ schema: "knowledge-store/v1", rootPath: "", git: { enabled: false, remoteUrl: "", branch: "main", autoCommit: false, autoPush: false } });
|
|
1429
|
+
const knowledgeMigrationPlan = ref([]);
|
|
1430
|
+
const loggingConfig = reactive({ schema: "logging/v1", rootPath: "", retentionDays: 30, levels: ["info", "warn", "error"] });
|
|
721
1431
|
const loading = ref(false);
|
|
722
1432
|
const pollError = ref("");
|
|
723
1433
|
const activeView = ref("dashboard");
|
|
@@ -725,6 +1435,63 @@ createApp({
|
|
|
725
1435
|
const theme = ref(localStorage.getItem("kb-theme") || "light");
|
|
726
1436
|
const uiLanguage = ref(localStorage.getItem("kb-ui-language") || "zh");
|
|
727
1437
|
|
|
1438
|
+
// ---- Claude terminal state ----
|
|
1439
|
+
const terminalLines = ref([]);
|
|
1440
|
+
const terminalSessionId = ref(null);
|
|
1441
|
+
const terminalSession = ref({ state: 'idle', model: null, aiProfileId: null, claudeSessionId: null, turns: 0 });
|
|
1442
|
+
const terminalSessions = ref([]);
|
|
1443
|
+
const terminalPermission = ref(null);
|
|
1444
|
+
const terminalPermissionBusy = ref(false);
|
|
1445
|
+
const terminalSystemPrompt = ref('');
|
|
1446
|
+
const terminalInput = ref('');
|
|
1447
|
+
const terminalScrollRef = ref(null);
|
|
1448
|
+
let claudeEventSource = null;
|
|
1449
|
+
// When assistant emits text deltas, accumulate into the last text-kind line so chunks
|
|
1450
|
+
// render as continuous prose instead of N tiny lines.
|
|
1451
|
+
let currentTextLineIdx = -1;
|
|
1452
|
+
let currentThinkingLineIdx = -1;
|
|
1453
|
+
let lastAssistantLineIdx = -1;
|
|
1454
|
+
const currentToolLineById = new Map();
|
|
1455
|
+
|
|
1456
|
+
// ---- Prompts editor (Step 5) ----
|
|
1457
|
+
const promptsConfig = ref(null);
|
|
1458
|
+
const promptsSaving = ref(false);
|
|
1459
|
+
const promptsError = ref('');
|
|
1460
|
+
const promptsSaved = ref(false);
|
|
1461
|
+
|
|
1462
|
+
async function loadPrompts() {
|
|
1463
|
+
promptsError.value = '';
|
|
1464
|
+
try {
|
|
1465
|
+
promptsConfig.value = await api("GET", "/api/prompts");
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
promptsError.value = e.message;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
async function savePrompts() {
|
|
1471
|
+
if (!promptsConfig.value) return;
|
|
1472
|
+
promptsSaving.value = true;
|
|
1473
|
+
promptsError.value = '';
|
|
1474
|
+
promptsSaved.value = false;
|
|
1475
|
+
try {
|
|
1476
|
+
const r = await api("PUT", "/api/prompts", promptsConfig.value);
|
|
1477
|
+
promptsSaved.value = true;
|
|
1478
|
+
setTimeout(() => { promptsSaved.value = false; }, 2500);
|
|
1479
|
+
} catch (e) {
|
|
1480
|
+
promptsError.value = e.message;
|
|
1481
|
+
} finally {
|
|
1482
|
+
promptsSaving.value = false;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
watch(() => activeView.value, (v) => {
|
|
1486
|
+
if (v !== 'dashboard') summaryPanel.value = "";
|
|
1487
|
+
if (v === 'prompts' && !promptsConfig.value) loadPrompts();
|
|
1488
|
+
if (v === 'logs') loadStructuredLogs().catch(() => {});
|
|
1489
|
+
if (v === 'settings') {
|
|
1490
|
+
loadKnowledgeStoreConfig().catch(() => {});
|
|
1491
|
+
loadLoggingConfig().catch(() => {});
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
|
|
728
1495
|
const runs = ref([]);
|
|
729
1496
|
const selectedRunId = ref("");
|
|
730
1497
|
const selectedRun = ref(null);
|
|
@@ -733,18 +1500,40 @@ createApp({
|
|
|
733
1500
|
const previewDraftPath = ref("");
|
|
734
1501
|
const draftPreview = ref("");
|
|
735
1502
|
const allowGoalEdit = ref(false);
|
|
1503
|
+
const draftBranchFilter = ref("");
|
|
1504
|
+
const removeDialog = reactive({ open: false, slug: "", preview: {}, deleteKb: false, reason: "", confirmText: "", error: "", busy: false });
|
|
736
1505
|
|
|
737
1506
|
const aiConfig = reactive({ schema: "ai-profiles/v1", defaultProfileId: "mock-agent", profiles: [] });
|
|
738
1507
|
const aiProfilesText = ref("");
|
|
739
1508
|
const adapters = ref([]);
|
|
740
1509
|
const aiMessage = ref("");
|
|
1510
|
+
const selectedConfigProfileId = ref("claude-code-agent");
|
|
1511
|
+
const profileForm = reactive({
|
|
1512
|
+
id: "",
|
|
1513
|
+
name: "",
|
|
1514
|
+
enabled: false,
|
|
1515
|
+
implementation: "",
|
|
1516
|
+
baseUrl: "",
|
|
1517
|
+
apiKey: "",
|
|
1518
|
+
apiKeyEnv: "",
|
|
1519
|
+
model: "",
|
|
1520
|
+
version: "2023-06-01",
|
|
1521
|
+
temperature: 0.2,
|
|
1522
|
+
maxTokens: 4096,
|
|
1523
|
+
timeoutMs: 300000,
|
|
1524
|
+
});
|
|
1525
|
+
const testPrompt = ref("what model are you?");
|
|
1526
|
+
const testResult = ref("");
|
|
1527
|
+
const testingProfile = ref(false);
|
|
741
1528
|
const selectedAiProfileId = ref("mock-agent");
|
|
742
1529
|
const selectedKnowledgeLanguage = ref("zh-CN");
|
|
743
1530
|
|
|
744
1531
|
const hookStatus = reactive({ installed: false, path: "", message: "" });
|
|
745
1532
|
const actionMessage = ref("");
|
|
1533
|
+
const advancedOpen = ref(false);
|
|
1534
|
+
const importPreflight = ref(null);
|
|
746
1535
|
|
|
747
|
-
const form = reactive({ slug: "", displayName: "", localPath: "", gitPath: "", primaryLanguage: "", tagsStr: "", isReference: false, initNow: true, knowledgeLanguage: "zh-CN" });
|
|
1536
|
+
const form = reactive({ slug: "", displayName: "", localPath: "", gitPath: "", primaryLanguage: "", tagsStr: "", isReference: false, initNow: true, initGit: false, createInitialCommit: false, remoteUrl: "", knowledgeLanguage: "zh-CN" });
|
|
748
1537
|
const submitting = ref(false);
|
|
749
1538
|
const formError = ref("");
|
|
750
1539
|
const formOk = ref("");
|
|
@@ -755,6 +1544,26 @@ createApp({
|
|
|
755
1544
|
|
|
756
1545
|
const projectList = computed(() => Object.entries(projects.value || {}).map(([slug, cfg]) => ({ slug, cfg })));
|
|
757
1546
|
const selectedProject = computed(() => selectedSlug.value && projects.value ? projects.value[selectedSlug.value] : null);
|
|
1547
|
+
const issuesByProject = computed(() => {
|
|
1548
|
+
const map = new Map();
|
|
1549
|
+
for (const issue of supervisionIssues.value || []) {
|
|
1550
|
+
const slug = issue.projectSlug || "system";
|
|
1551
|
+
if (!map.has(slug)) map.set(slug, { slug, displayName: slug === "system" ? "System" : ((projects.value && projects.value[slug] && projects.value[slug].displayName) || slug), issues: [] });
|
|
1552
|
+
map.get(slug).issues.push(issue);
|
|
1553
|
+
}
|
|
1554
|
+
return [...map.values()].map(group => {
|
|
1555
|
+
const hasError = group.issues.some(i => i.level === "error");
|
|
1556
|
+
const hasWarn = group.issues.some(i => i.level === "warn");
|
|
1557
|
+
return { ...group, hasWarnOrError: hasError || hasWarn, levelClass: hasError ? "bad" : hasWarn ? "warn" : "idle" };
|
|
1558
|
+
});
|
|
1559
|
+
});
|
|
1560
|
+
const draftBranches = computed(() => [...new Set((drafts.value || []).map(d => d.sourceBranch || t("unknownBranch")))].sort());
|
|
1561
|
+
const filteredDrafts = computed(() => {
|
|
1562
|
+
if (!draftBranchFilter.value) return drafts.value || [];
|
|
1563
|
+
return (drafts.value || []).filter(d => (d.sourceBranch || t("unknownBranch")) === draftBranchFilter.value);
|
|
1564
|
+
});
|
|
1565
|
+
const applyDraftButtonLabel = computed(() => draftBranchFilter.value ? `${t("applySelected")} · ${draftBranchFilter.value}` : t("applySelected"));
|
|
1566
|
+
const removeProjectDisabled = computed(() => removeDialog.busy || removeDialog.preview.hasRunningJobs || (removeDialog.deleteKb && removeDialog.confirmText !== removeDialog.slug));
|
|
758
1567
|
function t(key) {
|
|
759
1568
|
return (I18N[uiLanguage.value] && I18N[uiLanguage.value][key]) || I18N.en[key] || key;
|
|
760
1569
|
}
|
|
@@ -765,7 +1574,8 @@ createApp({
|
|
|
765
1574
|
ai: t("aiProfiles"),
|
|
766
1575
|
runs: t("runsAndDrafts"),
|
|
767
1576
|
schedule: t("schedule"),
|
|
768
|
-
logs: t("logs")
|
|
1577
|
+
logs: t("logs"),
|
|
1578
|
+
settings: t("settings")
|
|
769
1579
|
}[activeView.value] || t("projectSupervision")));
|
|
770
1580
|
|
|
771
1581
|
const summaryCards = computed(() => {
|
|
@@ -773,10 +1583,10 @@ createApp({
|
|
|
773
1583
|
const pending = list.reduce((sum, p) => sum + Number(p.cfg.lastScanPendingCount || 0), 0);
|
|
774
1584
|
const failed = jobHistory.value.filter(j => j.status === "failed" || j.status === "partial").length;
|
|
775
1585
|
return [
|
|
776
|
-
{ label: t("projects"), value: list.length, note: t("registeredProjects") },
|
|
777
|
-
{ label: t("runningJobs"), value: runningJobs.value.length, note: t("activeBackendJobs") },
|
|
778
|
-
{ label: t("pendingCommits"), value: pending, note: t("pendingFromLastScan") },
|
|
779
|
-
{ label: t("recentIssues"), value: failed, note: t("failedOrPartialJobs") }
|
|
1586
|
+
{ key: "projects", label: t("projects"), value: list.length, note: t("registeredProjects") },
|
|
1587
|
+
{ key: "running", label: t("runningJobs"), value: runningJobs.value.length, note: t("activeBackendJobs") },
|
|
1588
|
+
{ key: "pending", label: t("pendingCommits"), value: pending, note: t("pendingFromLastScan") },
|
|
1589
|
+
{ key: "issues", label: t("recentIssues"), value: supervisionIssues.value.length || failed, note: t("failedOrPartialJobs") }
|
|
780
1590
|
];
|
|
781
1591
|
});
|
|
782
1592
|
|
|
@@ -786,7 +1596,8 @@ createApp({
|
|
|
786
1596
|
{ key: "ai", label: t("aiProfiles") },
|
|
787
1597
|
{ key: "runs", label: t("runsDrafts"), badge: runs.value.length || "" },
|
|
788
1598
|
{ key: "schedule", label: t("schedule") },
|
|
789
|
-
{ key: "logs", label: t("logs"), badge: lastRun.value.status === "failed" ? "failed" : "" }
|
|
1599
|
+
{ key: "logs", label: t("logs"), badge: lastRun.value.status === "failed" ? "failed" : "" },
|
|
1600
|
+
{ key: "settings", label: t("settings") }
|
|
790
1601
|
]);
|
|
791
1602
|
|
|
792
1603
|
const logOutput = computed(() => selectedJob.value ? (selectedJob.value.output || "") : (lastRun.value.output || ""));
|
|
@@ -809,12 +1620,12 @@ createApp({
|
|
|
809
1620
|
}
|
|
810
1621
|
|
|
811
1622
|
function projectKbPath(slug) {
|
|
812
|
-
const root = trimTrailingSlash(
|
|
813
|
-
return `${root}
|
|
1623
|
+
const root = trimTrailingSlash(knowledgeStoreConfig.rootPath || `${kbRoot.value}\\projects`);
|
|
1624
|
+
return `${root}\\${slug}`;
|
|
814
1625
|
}
|
|
815
1626
|
|
|
816
1627
|
function displayProjectKbPath(slug) {
|
|
817
|
-
return `${trimTrailingSlash(
|
|
1628
|
+
return `${trimTrailingSlash(knowledgeStoreConfig.rootPath || "knowledge-store")}\\${slug}`;
|
|
818
1629
|
}
|
|
819
1630
|
|
|
820
1631
|
function fileUrlFromPath(value) {
|
|
@@ -827,15 +1638,32 @@ createApp({
|
|
|
827
1638
|
try { return new Date(value).toLocaleString(); } catch { return value; }
|
|
828
1639
|
}
|
|
829
1640
|
|
|
1641
|
+
function formatBytes(bytes) {
|
|
1642
|
+
const n = Number(bytes || 0);
|
|
1643
|
+
if (n < 1024) return `${n} B`;
|
|
1644
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
1645
|
+
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
1646
|
+
return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
830
1649
|
function selectProject(slug) {
|
|
1650
|
+
summaryPanel.value = "";
|
|
831
1651
|
selectedSlug.value = slug;
|
|
832
1652
|
selectedRunId.value = "";
|
|
833
1653
|
selectedRun.value = null;
|
|
834
1654
|
drafts.value = [];
|
|
1655
|
+
draftBranchFilter.value = "";
|
|
835
1656
|
previewDraftPath.value = "";
|
|
836
1657
|
draftPreview.value = "";
|
|
1658
|
+
closeEventSource();
|
|
1659
|
+
terminalLines.value = [];
|
|
1660
|
+
resetChatPointers();
|
|
1661
|
+
terminalSessionId.value = null;
|
|
1662
|
+
terminalPermission.value = null;
|
|
1663
|
+
terminalSession.value = { state: 'idle', model: null, aiProfileId: projects.value && projects.value[slug] && projects.value[slug].aiProfileId || null, claudeSessionId: null, turns: 0 };
|
|
837
1664
|
loadRuns(slug);
|
|
838
1665
|
loadHookStatus(slug);
|
|
1666
|
+
loadClaudeSessions(slug, true);
|
|
839
1667
|
}
|
|
840
1668
|
|
|
841
1669
|
function isKbInit(slug) {
|
|
@@ -882,7 +1710,8 @@ createApp({
|
|
|
882
1710
|
|
|
883
1711
|
function goalUrl(slug) {
|
|
884
1712
|
const cfg = projects.value && projects.value[slug];
|
|
885
|
-
|
|
1713
|
+
const goalFile = cfg && cfg.kbSchemaVersion === "v3" ? "GOAL.md" : "project-goal.md";
|
|
1714
|
+
return fileUrlFromPath(`${(cfg && cfg.kbPath) || projectKbPath(slug)}\\${goalFile}`);
|
|
886
1715
|
}
|
|
887
1716
|
|
|
888
1717
|
async function api(method, url, body) {
|
|
@@ -911,9 +1740,11 @@ createApp({
|
|
|
911
1740
|
kbRoot.value = state.kbRoot;
|
|
912
1741
|
projects.value = state.projects;
|
|
913
1742
|
schedule.value = state.schedule;
|
|
1743
|
+
if (state.knowledgeStore) Object.assign(knowledgeStoreConfig, state.knowledgeStore);
|
|
1744
|
+
if (state.logging) Object.assign(loggingConfig, state.logging);
|
|
914
1745
|
lastRun.value = state.lastRun || lastRun.value;
|
|
915
1746
|
if (!selectedSlug.value && projectList.value.length) selectProject(projectList.value[0].slug);
|
|
916
|
-
await Promise.all([loadJobs(), loadAiProfiles(false)]);
|
|
1747
|
+
await Promise.all([loadJobs(), loadAiProfiles(false), loadIssues()]);
|
|
917
1748
|
} catch (e) {
|
|
918
1749
|
pollError.value = e.message;
|
|
919
1750
|
} finally {
|
|
@@ -921,6 +1752,393 @@ createApp({
|
|
|
921
1752
|
}
|
|
922
1753
|
}
|
|
923
1754
|
|
|
1755
|
+
// ---- Claude terminal ----
|
|
1756
|
+
const terminalStatusDot = computed(() => {
|
|
1757
|
+
const s = terminalSession.value.state;
|
|
1758
|
+
if (s === 'running' || s === 'spawning' || s === 'pending-permission') return 'warn';
|
|
1759
|
+
if (s === 'failed' || s === 'aborted') return 'bad';
|
|
1760
|
+
if (s === 'idle' && terminalSessionId.value) return 'good';
|
|
1761
|
+
return 'idle';
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
const terminalInputPlaceholder = computed(() => {
|
|
1765
|
+
if (!terminalSessionId.value) return t('terminalNeedSession');
|
|
1766
|
+
if (!terminalSession.value.claudeSessionId) return t('terminalWaitInit');
|
|
1767
|
+
if (terminalSession.value.state === 'pending-permission') return t('terminalNeedPermission');
|
|
1768
|
+
if (terminalSession.value.state === 'running' || terminalSession.value.state === 'spawning') return t('terminalBusy');
|
|
1769
|
+
return t('terminalInputPlaceholder');
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
const permissionSummaryText = computed(() => {
|
|
1773
|
+
const req = terminalPermission.value;
|
|
1774
|
+
if (!req) return '';
|
|
1775
|
+
const s = req.summary || {};
|
|
1776
|
+
const tools = Array.isArray(s.allowedTools) && s.allowedTools.length ? s.allowedTools.join(', ') : '-';
|
|
1777
|
+
return [
|
|
1778
|
+
`${t('permissionTurn')}: ${s.turnKind || '-'}`,
|
|
1779
|
+
`${t('permissionMode')}: ${s.permissionMode || '-'}`,
|
|
1780
|
+
`${t('permissionTools')}: ${tools}`,
|
|
1781
|
+
`${t('permissionCwd')}: ${s.cwd || '-'}`,
|
|
1782
|
+
].join('\n');
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
function appendTerminalLine(line) {
|
|
1786
|
+
terminalLines.value.push(line);
|
|
1787
|
+
// Trim very long buffers
|
|
1788
|
+
if (terminalLines.value.length > 4000) terminalLines.value.splice(0, 500);
|
|
1789
|
+
nextTick(() => {
|
|
1790
|
+
const el = terminalScrollRef.value;
|
|
1791
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function resetChatPointers() {
|
|
1796
|
+
currentTextLineIdx = -1;
|
|
1797
|
+
currentThinkingLineIdx = -1;
|
|
1798
|
+
lastAssistantLineIdx = -1;
|
|
1799
|
+
currentToolLineById.clear();
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function escapeHtml(value) {
|
|
1803
|
+
return String(value || '')
|
|
1804
|
+
.replace(/&/g, '&')
|
|
1805
|
+
.replace(/</g, '<')
|
|
1806
|
+
.replace(/>/g, '>')
|
|
1807
|
+
.replace(/"/g, '"')
|
|
1808
|
+
.replace(/'/g, ''');
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function renderInlineMarkdown(value) {
|
|
1812
|
+
return escapeHtml(value)
|
|
1813
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
1814
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
1815
|
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function renderMarkdown(value) {
|
|
1819
|
+
const text = String(value || '').replace(/\r\n/g, '\n');
|
|
1820
|
+
const codeBlocks = [];
|
|
1821
|
+
const protectedText = text.replace(/```([\w-]*)\n?([\s\S]*?)```/g, (_, lang, code) => {
|
|
1822
|
+
const token = `@@CODE_BLOCK_${codeBlocks.length}@@`;
|
|
1823
|
+
codeBlocks.push(`<pre><code>${escapeHtml(code.trimEnd())}</code></pre>`);
|
|
1824
|
+
return token;
|
|
1825
|
+
});
|
|
1826
|
+
const blocks = protectedText.split(/\n{2,}/).map(block => block.trim()).filter(Boolean);
|
|
1827
|
+
return blocks.map(block => {
|
|
1828
|
+
const codeIndex = block.match(/^@@CODE_BLOCK_(\d+)@@$/);
|
|
1829
|
+
if (codeIndex) return codeBlocks[Number(codeIndex[1])] || '';
|
|
1830
|
+
if (/^###\s+/.test(block)) return `<h3>${renderInlineMarkdown(block.replace(/^###\s+/, ''))}</h3>`;
|
|
1831
|
+
if (/^##\s+/.test(block)) return `<h2>${renderInlineMarkdown(block.replace(/^##\s+/, ''))}</h2>`;
|
|
1832
|
+
if (/^#\s+/.test(block)) return `<h1>${renderInlineMarkdown(block.replace(/^#\s+/, ''))}</h1>`;
|
|
1833
|
+
const lines = block.split('\n');
|
|
1834
|
+
if (lines.every(line => /^\s*[-*]\s+/.test(line))) {
|
|
1835
|
+
return `<ul>${lines.map(line => `<li>${renderInlineMarkdown(line.replace(/^\s*[-*]\s+/, ''))}</li>`).join('')}</ul>`;
|
|
1836
|
+
}
|
|
1837
|
+
if (lines.every(line => /^\s*\d+\.\s+/.test(line))) {
|
|
1838
|
+
return `<ol>${lines.map(line => `<li>${renderInlineMarkdown(line.replace(/^\s*\d+\.\s+/, ''))}</li>`).join('')}</ol>`;
|
|
1839
|
+
}
|
|
1840
|
+
if (lines.every(line => /^\s*>\s?/.test(line))) {
|
|
1841
|
+
return `<blockquote>${lines.map(line => renderInlineMarkdown(line.replace(/^\s*>\s?/, ''))).join('<br>')}</blockquote>`;
|
|
1842
|
+
}
|
|
1843
|
+
return `<p>${lines.map(renderInlineMarkdown).join('<br>')}</p>`;
|
|
1844
|
+
}).join('');
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function appendAssistantText(text) {
|
|
1848
|
+
if (!text) return;
|
|
1849
|
+
const idx = currentTextLineIdx >= 0 ? currentTextLineIdx : -1;
|
|
1850
|
+
if (idx >= 0 && idx < terminalLines.value.length && terminalLines.value[idx].kind === 'assistant') {
|
|
1851
|
+
terminalLines.value[idx].text += text;
|
|
1852
|
+
lastAssistantLineIdx = idx;
|
|
1853
|
+
} else {
|
|
1854
|
+
currentTextLineIdx = terminalLines.value.length;
|
|
1855
|
+
lastAssistantLineIdx = currentTextLineIdx;
|
|
1856
|
+
appendTerminalLine({ kind: 'assistant', text });
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
function setAssistantResult(text) {
|
|
1861
|
+
if (!text) return;
|
|
1862
|
+
const idx = lastAssistantLineIdx >= 0 ? lastAssistantLineIdx : currentTextLineIdx;
|
|
1863
|
+
if (idx >= 0 && idx < terminalLines.value.length && terminalLines.value[idx].kind === 'assistant') {
|
|
1864
|
+
terminalLines.value[idx].text = text;
|
|
1865
|
+
} else {
|
|
1866
|
+
lastAssistantLineIdx = terminalLines.value.length;
|
|
1867
|
+
appendTerminalLine({ kind: 'assistant', text });
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function permissionSummary(req) {
|
|
1872
|
+
if (!req) return '';
|
|
1873
|
+
const s = req.summary || {};
|
|
1874
|
+
const tools = Array.isArray(s.allowedTools) && s.allowedTools.length ? s.allowedTools.join(', ') : '-';
|
|
1875
|
+
return [
|
|
1876
|
+
`${t('permissionTurn')}: ${s.turnKind || '-'}`,
|
|
1877
|
+
`${t('permissionMode')}: ${s.permissionMode || '-'}`,
|
|
1878
|
+
`${t('permissionTools')}: ${tools}`,
|
|
1879
|
+
`${t('permissionCwd')}: ${s.cwd || '-'}`,
|
|
1880
|
+
].join('\n');
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function summaryOfToolInput(input) {
|
|
1884
|
+
if (!input || typeof input !== 'object') return '';
|
|
1885
|
+
const keys = Object.keys(input);
|
|
1886
|
+
if (!keys.length) return '';
|
|
1887
|
+
// Show short, common fields nicely
|
|
1888
|
+
if (typeof input.path === 'string') return input.path;
|
|
1889
|
+
if (typeof input.pattern === 'string') return `pattern=${input.pattern}`;
|
|
1890
|
+
if (typeof input.command === 'string') return `$ ${input.command}`;
|
|
1891
|
+
if (typeof input.prompt === 'string') return input.prompt.slice(0, 120);
|
|
1892
|
+
// Fallback: short JSON
|
|
1893
|
+
const s = JSON.stringify(input);
|
|
1894
|
+
if (s.length > 200) return s.slice(0, 200) + '...';
|
|
1895
|
+
return s;
|
|
1896
|
+
return s.length > 200 ? s.slice(0, 200) + '…' : s;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function closeEventSource() {
|
|
1900
|
+
if (claudeEventSource) {
|
|
1901
|
+
try { claudeEventSource.close(); } catch {}
|
|
1902
|
+
claudeEventSource = null;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function subscribeClaudeSession(sessionId) {
|
|
1907
|
+
closeEventSource();
|
|
1908
|
+
claudeEventSource = new EventSource(`/api/claude/sessions/${sessionId}/stream`);
|
|
1909
|
+
|
|
1910
|
+
const handle = (typeName, fn) => {
|
|
1911
|
+
claudeEventSource.addEventListener(typeName, (e) => {
|
|
1912
|
+
let data; try { data = JSON.parse(e.data); } catch { return; }
|
|
1913
|
+
try { fn(data); } catch (err) { console.error('terminal handler', typeName, err); }
|
|
1914
|
+
});
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1917
|
+
handle('claude/system-prompt', (d) => {
|
|
1918
|
+
currentTextLineIdx = -1;
|
|
1919
|
+
appendTerminalLine({ kind: 'status', text: t('systemPromptLoaded') });
|
|
1920
|
+
});
|
|
1921
|
+
handle('claude/user-prompt', (d) => {
|
|
1922
|
+
currentTextLineIdx = -1;
|
|
1923
|
+
appendTerminalLine({ kind: 'user', text: d.text || '' });
|
|
1924
|
+
});
|
|
1925
|
+
handle('claude/init', (d) => {
|
|
1926
|
+
terminalSession.value = { ...terminalSession.value, claudeSessionId: d.claudeSessionId, model: d.model, aiProfileId: d.aiProfileId || terminalSession.value.aiProfileId };
|
|
1927
|
+
appendTerminalLine({ kind: 'status', text: `Claude session initialized · ${d.model || '?'} · ${(d.claudeSessionId || '').slice(0, 8)}` });
|
|
1928
|
+
});
|
|
1929
|
+
handle('claude/restored', (d) => {
|
|
1930
|
+
terminalSession.value = { ...terminalSession.value, state: d.state || terminalSession.value.state };
|
|
1931
|
+
appendTerminalLine({ kind: 'status', text: `Session restored from ${d.fromState || 'disk'}` });
|
|
1932
|
+
});
|
|
1933
|
+
handle('claude/permission-request', (d) => {
|
|
1934
|
+
terminalPermission.value = d;
|
|
1935
|
+
terminalSession.value = { ...terminalSession.value, state: 'pending-permission', pendingPermission: d };
|
|
1936
|
+
appendTerminalLine({ kind: 'permission', requestId: d.requestId, summaryText: permissionSummary(d) });
|
|
1937
|
+
});
|
|
1938
|
+
handle('claude/permission-resolved', (d) => {
|
|
1939
|
+
if (terminalPermission.value && terminalPermission.value.requestId === d.requestId) {
|
|
1940
|
+
terminalPermission.value = null;
|
|
1941
|
+
}
|
|
1942
|
+
appendTerminalLine({ kind: 'status', text: d.allow ? t('permissionApproved') : t('permissionDenied') });
|
|
1943
|
+
});
|
|
1944
|
+
handle('claude/message-start', () => {
|
|
1945
|
+
currentTextLineIdx = -1;
|
|
1946
|
+
currentThinkingLineIdx = -1;
|
|
1947
|
+
});
|
|
1948
|
+
handle('claude/thinking-start', () => {
|
|
1949
|
+
currentThinkingLineIdx = terminalLines.value.length;
|
|
1950
|
+
appendTerminalLine({ kind: 'thinking', text: '' });
|
|
1951
|
+
});
|
|
1952
|
+
handle('claude/thinking-delta', (d) => {
|
|
1953
|
+
if (currentThinkingLineIdx >= 0 && currentThinkingLineIdx < terminalLines.value.length) {
|
|
1954
|
+
terminalLines.value[currentThinkingLineIdx].text += d.text || '';
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
handle('claude/text-delta', (d) => {
|
|
1958
|
+
appendAssistantText(d.text || '');
|
|
1959
|
+
});
|
|
1960
|
+
handle('claude/tool-use', (d) => {
|
|
1961
|
+
currentTextLineIdx = -1;
|
|
1962
|
+
const line = {
|
|
1963
|
+
kind: 'tool',
|
|
1964
|
+
id: d.id,
|
|
1965
|
+
name: d.name,
|
|
1966
|
+
summary: summaryOfToolInput(d.input),
|
|
1967
|
+
inputJson: d.input ? JSON.stringify(d.input, null, 2) : '',
|
|
1968
|
+
open: false,
|
|
1969
|
+
};
|
|
1970
|
+
if (d.id && currentToolLineById.has(d.id)) {
|
|
1971
|
+
const idx = currentToolLineById.get(d.id);
|
|
1972
|
+
terminalLines.value[idx] = { ...terminalLines.value[idx], ...line };
|
|
1973
|
+
} else {
|
|
1974
|
+
currentToolLineById.set(d.id || `tool-${terminalLines.value.length}`, terminalLines.value.length);
|
|
1975
|
+
appendTerminalLine(line);
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
handle('claude/tool-use-start', (d) => {
|
|
1979
|
+
currentTextLineIdx = -1;
|
|
1980
|
+
const id = d.id || `tool-${terminalLines.value.length}`;
|
|
1981
|
+
currentToolLineById.set(id, terminalLines.value.length);
|
|
1982
|
+
appendTerminalLine({ kind: 'tool', id, name: d.name, summary: t('toolRunning'), inputJson: '', open: false });
|
|
1983
|
+
return;
|
|
1984
|
+
appendTerminalLine({ kind: 'tool', name: d.name, summary: '(input streaming…)' });
|
|
1985
|
+
});
|
|
1986
|
+
handle('claude/tool-input-delta', () => {
|
|
1987
|
+
// Too chatty to print; the final tool-use event carries the assembled input.
|
|
1988
|
+
});
|
|
1989
|
+
handle('claude/message-stop', (d) => {
|
|
1990
|
+
currentTextLineIdx = -1;
|
|
1991
|
+
currentThinkingLineIdx = -1;
|
|
1992
|
+
if (d.stopReason) appendTerminalLine({ kind: 'status', text: `message stop: ${d.stopReason}` });
|
|
1993
|
+
});
|
|
1994
|
+
handle('claude/state', (d) => {
|
|
1995
|
+
terminalSession.value = { ...terminalSession.value, state: d.state };
|
|
1996
|
+
if (d.turn != null) terminalSession.value.turns = d.turn;
|
|
1997
|
+
if (d.state !== 'pending-permission') terminalPermission.value = null;
|
|
1998
|
+
if (d.state === 'idle' || d.state === 'failed' || d.state === 'aborted') {
|
|
1999
|
+
appendTerminalLine({ kind: 'status', text: `${d.state}${d.exitCode != null ? ` (exit ${d.exitCode})` : ''}` });
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
handle('claude/result', (d) => {
|
|
2003
|
+
if (d.result) setAssistantResult(d.result);
|
|
2004
|
+
return;
|
|
2005
|
+
const head = (d.result || '').slice(0, 1500);
|
|
2006
|
+
appendTerminalLine({ kind: 'result', text: head + (d.result && d.result.length > 1500 ? `\n… (+${d.result.length - 1500} chars)` : '') });
|
|
2007
|
+
});
|
|
2008
|
+
handle('claude/stderr', (d) => {
|
|
2009
|
+
if (d.text && !d.text.includes('DeprecationWarning')) {
|
|
2010
|
+
appendTerminalLine({ kind: 'error', text: d.text });
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
handle('claude/error', (d) => appendTerminalLine({ kind: 'error', text: d.message }));
|
|
2014
|
+
handle('claude/aborted', () => appendTerminalLine({ kind: 'status', text: 'aborted by user' }));
|
|
2015
|
+
handle('claude/exit', (d) => {
|
|
2016
|
+
if (d.exitCode !== 0) appendTerminalLine({ kind: 'status', text: `Claude exited with code ${d.exitCode}` });
|
|
2017
|
+
});
|
|
2018
|
+
// Suppress claude/raw (hook lifecycle noise) — don't add a handler.
|
|
2019
|
+
|
|
2020
|
+
claudeEventSource.onerror = () => {
|
|
2021
|
+
// EventSource auto-reconnects; only log
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
async function loadClaudeSessions(slug, autoRestore) {
|
|
2026
|
+
if (!slug) return;
|
|
2027
|
+
try {
|
|
2028
|
+
const data = await api("GET", `/api/claude/sessions?slug=${encodeURIComponent(slug)}`);
|
|
2029
|
+
terminalSessions.value = data.sessions || [];
|
|
2030
|
+
if (autoRestore && terminalSessions.value.length && !terminalSessionId.value) {
|
|
2031
|
+
await restoreLatestClaudeSession(true);
|
|
2032
|
+
}
|
|
2033
|
+
} catch (e) {
|
|
2034
|
+
terminalSessions.value = [];
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
async function restoreLatestClaudeSession(silent) {
|
|
2039
|
+
if (!selectedSlug.value) return;
|
|
2040
|
+
if (!terminalSessions.value.length) {
|
|
2041
|
+
await loadClaudeSessions(selectedSlug.value, false);
|
|
2042
|
+
}
|
|
2043
|
+
const latest = terminalSessions.value[0];
|
|
2044
|
+
if (!latest) {
|
|
2045
|
+
if (!silent) appendTerminalLine({ kind: 'state', text: t('noRestorableSession') });
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
closeEventSource();
|
|
2049
|
+
terminalLines.value = [];
|
|
2050
|
+
resetChatPointers();
|
|
2051
|
+
terminalPermission.value = latest.pendingPermission || null;
|
|
2052
|
+
terminalSessionId.value = latest.sessionId;
|
|
2053
|
+
terminalSession.value = {
|
|
2054
|
+
state: latest.state || 'idle',
|
|
2055
|
+
model: latest.model || null,
|
|
2056
|
+
aiProfileId: latest.aiProfileId || (selectedProject.value && selectedProject.value.aiProfileId) || null,
|
|
2057
|
+
claudeSessionId: latest.claudeSessionId || null,
|
|
2058
|
+
turns: latest.turns || 0,
|
|
2059
|
+
pendingPermission: latest.pendingPermission || null,
|
|
2060
|
+
restored: latest.restored || false,
|
|
2061
|
+
};
|
|
2062
|
+
localStorage.setItem(`kb-claude-session-${selectedSlug.value}`, latest.sessionId);
|
|
2063
|
+
subscribeClaudeSession(latest.sessionId);
|
|
2064
|
+
if (!silent) appendTerminalLine({ kind: 'state', text: t('sessionRecovered') });
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
async function startClaudeAnalysis() {
|
|
2068
|
+
if (!selectedSlug.value) return;
|
|
2069
|
+
closeEventSource();
|
|
2070
|
+
terminalLines.value = [];
|
|
2071
|
+
resetChatPointers();
|
|
2072
|
+
terminalPermission.value = null;
|
|
2073
|
+
terminalSession.value = { state: 'spawning', model: null, aiProfileId: selectedProject.value && selectedProject.value.aiProfileId || null, claudeSessionId: null, turns: 0 };
|
|
2074
|
+
try {
|
|
2075
|
+
const r = await api("POST", `/api/projects/${selectedSlug.value}/analyze/initial?mode=cli`);
|
|
2076
|
+
if (!r.sessionId) throw new Error('server returned no sessionId');
|
|
2077
|
+
terminalSessionId.value = r.sessionId;
|
|
2078
|
+
terminalPermission.value = r.pendingPermission || null;
|
|
2079
|
+
terminalSession.value = { ...terminalSession.value, aiProfileId: r.aiProfileId || terminalSession.value.aiProfileId, pendingPermission: r.pendingPermission || null };
|
|
2080
|
+
localStorage.setItem(`kb-claude-session-${selectedSlug.value}`, r.sessionId);
|
|
2081
|
+
subscribeClaudeSession(r.sessionId);
|
|
2082
|
+
await loadClaudeSessions(selectedSlug.value, false);
|
|
2083
|
+
} catch (e) {
|
|
2084
|
+
appendTerminalLine({ kind: 'error', text: 'failed to start: ' + e.message });
|
|
2085
|
+
terminalSession.value.state = 'failed';
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
async function sendClaudeInput() {
|
|
2090
|
+
const text = terminalInput.value.trim();
|
|
2091
|
+
if (!text || !terminalSessionId.value) return;
|
|
2092
|
+
if (terminalSession.value.state === 'running' || terminalSession.value.state === 'spawning') return;
|
|
2093
|
+
terminalInput.value = '';
|
|
2094
|
+
try {
|
|
2095
|
+
const r = await api("POST", `/api/claude/sessions/${terminalSessionId.value}/input`, { text });
|
|
2096
|
+
terminalPermission.value = r.pendingPermission || null;
|
|
2097
|
+
} catch (e) {
|
|
2098
|
+
appendTerminalLine({ kind: 'error', text: 'send failed: ' + e.message });
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
async function resolveClaudePermission(allow, requestId) {
|
|
2103
|
+
if (!terminalSessionId.value || !terminalPermission.value) return;
|
|
2104
|
+
const targetRequestId = requestId || terminalPermission.value.requestId;
|
|
2105
|
+
if (!targetRequestId || targetRequestId !== terminalPermission.value.requestId) return;
|
|
2106
|
+
terminalPermissionBusy.value = true;
|
|
2107
|
+
try {
|
|
2108
|
+
await api("POST", `/api/claude/sessions/${terminalSessionId.value}/permission`, {
|
|
2109
|
+
requestId: targetRequestId,
|
|
2110
|
+
allow,
|
|
2111
|
+
});
|
|
2112
|
+
} catch (e) {
|
|
2113
|
+
appendTerminalLine({ kind: 'error', text: 'permission failed: ' + e.message });
|
|
2114
|
+
} finally {
|
|
2115
|
+
terminalPermissionBusy.value = false;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
function approveClaudePermission(requestId) {
|
|
2120
|
+
return resolveClaudePermission(true, requestId);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function denyClaudePermission(requestId) {
|
|
2124
|
+
return resolveClaudePermission(false, requestId);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
async function stopClaudeSession() {
|
|
2128
|
+
if (!terminalSessionId.value) return;
|
|
2129
|
+
try {
|
|
2130
|
+
await api("POST", `/api/claude/sessions/${terminalSessionId.value}/abort`);
|
|
2131
|
+
} catch (e) {
|
|
2132
|
+
appendTerminalLine({ kind: 'error', text: 'abort failed: ' + e.message });
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
function clearTerminal() {
|
|
2137
|
+
terminalLines.value = [];
|
|
2138
|
+
terminalPermission.value = null;
|
|
2139
|
+
resetChatPointers();
|
|
2140
|
+
}
|
|
2141
|
+
|
|
924
2142
|
async function loadJobs() {
|
|
925
2143
|
const data = await api("GET", "/api/jobs");
|
|
926
2144
|
runningJobs.value = data.running || [];
|
|
@@ -929,6 +2147,85 @@ createApp({
|
|
|
929
2147
|
return data;
|
|
930
2148
|
}
|
|
931
2149
|
|
|
2150
|
+
async function loadIssues() {
|
|
2151
|
+
const data = await api("GET", "/api/supervision/issues");
|
|
2152
|
+
supervisionIssues.value = data.issues || [];
|
|
2153
|
+
return data;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
async function loadPendingCommits() {
|
|
2157
|
+
const data = await api("GET", "/api/supervision/pending-commits");
|
|
2158
|
+
pendingCommitItems.value = data.items || [];
|
|
2159
|
+
return data;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
async function openSummaryPanel(key) {
|
|
2163
|
+
if (key === "pending") {
|
|
2164
|
+
summaryPanel.value = summaryPanel.value === "pending" ? "" : "pending";
|
|
2165
|
+
if (summaryPanel.value) await loadPendingCommits();
|
|
2166
|
+
} else if (key === "issues") {
|
|
2167
|
+
summaryPanel.value = summaryPanel.value === "issues" ? "" : "issues";
|
|
2168
|
+
if (summaryPanel.value) await loadIssues();
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
async function loadStructuredLogs() {
|
|
2173
|
+
const params = new URLSearchParams();
|
|
2174
|
+
if (logFilters.dateFrom) {
|
|
2175
|
+
params.set("dateFrom", logFilters.dateFrom);
|
|
2176
|
+
params.set("dateTo", logFilters.dateFrom);
|
|
2177
|
+
}
|
|
2178
|
+
if (logFilters.level) params.set("level", logFilters.level);
|
|
2179
|
+
if (logFilters.projectSlug) params.set("projectSlug", logFilters.projectSlug);
|
|
2180
|
+
if (logFilters.source) params.set("source", logFilters.source);
|
|
2181
|
+
if (logFilters.q) params.set("q", logFilters.q);
|
|
2182
|
+
const data = await api("GET", `/api/logs?${params.toString()}`);
|
|
2183
|
+
structuredLogs.value = data.logs || [];
|
|
2184
|
+
if (data.config) Object.assign(loggingConfig, data.config);
|
|
2185
|
+
selectedLog.value = structuredLogs.value[0] || null;
|
|
2186
|
+
return data;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
async function loadKnowledgeStoreConfig() {
|
|
2190
|
+
const data = await api("GET", "/api/knowledge-store/config");
|
|
2191
|
+
if (data.config) Object.assign(knowledgeStoreConfig, data.config);
|
|
2192
|
+
if (!knowledgeStoreConfig.git) knowledgeStoreConfig.git = { enabled: false, remoteUrl: "", branch: "main", autoCommit: false, autoPush: false };
|
|
2193
|
+
return data;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
async function saveKnowledgeStoreConfig() {
|
|
2197
|
+
const data = await api("PUT", "/api/knowledge-store/config", knowledgeStoreConfig);
|
|
2198
|
+
if (data.config) Object.assign(knowledgeStoreConfig, data.config);
|
|
2199
|
+
await refreshAll();
|
|
2200
|
+
return data;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
async function previewKnowledgeMigration() {
|
|
2204
|
+
const data = await api("POST", "/api/knowledge-store/migrate", { execute: false });
|
|
2205
|
+
knowledgeMigrationPlan.value = data.plan || [];
|
|
2206
|
+
return data;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
async function executeKnowledgeMigration() {
|
|
2210
|
+
const data = await api("POST", "/api/knowledge-store/migrate", { execute: true, overwrite: false, move: false });
|
|
2211
|
+
knowledgeMigrationPlan.value = data.migrated || [];
|
|
2212
|
+
await refreshAll();
|
|
2213
|
+
return data;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
async function loadLoggingConfig() {
|
|
2217
|
+
const data = await api("GET", "/api/logging/config");
|
|
2218
|
+
if (data.config) Object.assign(loggingConfig, data.config);
|
|
2219
|
+
return data;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
async function saveLoggingConfig() {
|
|
2223
|
+
const data = await api("PUT", "/api/logging/config", loggingConfig);
|
|
2224
|
+
if (data.config) Object.assign(loggingConfig, data.config);
|
|
2225
|
+
await loadStructuredLogs();
|
|
2226
|
+
return data;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
932
2229
|
async function loadJobDetail(jobId) {
|
|
933
2230
|
const data = await api("GET", `/api/jobs/${jobId}`);
|
|
934
2231
|
selectedJob.value = data.job;
|
|
@@ -940,9 +2237,127 @@ createApp({
|
|
|
940
2237
|
Object.assign(aiConfig, data.config || {});
|
|
941
2238
|
adapters.value = data.adapters || [];
|
|
942
2239
|
if (updateText || !aiProfilesText.value.trim()) aiProfilesText.value = JSON.stringify(aiConfig, null, 2);
|
|
2240
|
+
if (!aiConfig.profiles.find(p => p.id === selectedConfigProfileId.value)) {
|
|
2241
|
+
selectedConfigProfileId.value = aiConfig.defaultProfileId || (aiConfig.profiles[0] && aiConfig.profiles[0].id) || "mock-agent";
|
|
2242
|
+
}
|
|
2243
|
+
loadProfileForm();
|
|
943
2244
|
return data;
|
|
944
2245
|
}
|
|
945
2246
|
|
|
2247
|
+
function loadProfileForm() {
|
|
2248
|
+
const profile = aiConfig.profiles.find(p => p.id === selectedConfigProfileId.value) || {};
|
|
2249
|
+
Object.assign(profileForm, {
|
|
2250
|
+
id: profile.id || selectedConfigProfileId.value || "",
|
|
2251
|
+
name: profile.name || profile.id || "",
|
|
2252
|
+
enabled: !!profile.enabled,
|
|
2253
|
+
implementation: profile.implementation || profile.id || "",
|
|
2254
|
+
baseUrl: profile.baseUrl || "",
|
|
2255
|
+
apiKey: profile.apiKey || "",
|
|
2256
|
+
apiKeyEnv: profile.apiKeyEnv || "",
|
|
2257
|
+
model: profile.model || "",
|
|
2258
|
+
version: profile.version || "2023-06-01",
|
|
2259
|
+
temperature: profile.temperature != null ? Number(profile.temperature) : 0.2,
|
|
2260
|
+
maxTokens: profile.maxTokens != null ? Number(profile.maxTokens) : 4096,
|
|
2261
|
+
timeoutMs: profile.timeoutMs != null ? Number(profile.timeoutMs) : 300000,
|
|
2262
|
+
});
|
|
2263
|
+
testResult.value = "";
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function normalizeProfileId(value) {
|
|
2267
|
+
return String(value || "")
|
|
2268
|
+
.trim()
|
|
2269
|
+
.toLowerCase()
|
|
2270
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
2271
|
+
.replace(/^-+|-+$/g, "");
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function profileOptionLabel(profile) {
|
|
2275
|
+
if (!profile) return "-";
|
|
2276
|
+
const state = profile.enabled === false ? t("disabledStatus") : t("enabledStatus");
|
|
2277
|
+
return `${profile.id} - ${profile.name || profile.id} (${state})`;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function syncProfileFormToConfig() {
|
|
2281
|
+
const profile = aiConfig.profiles.find(p => p.id === selectedConfigProfileId.value);
|
|
2282
|
+
if (!profile) throw new Error(`Profile not found: ${selectedConfigProfileId.value}`);
|
|
2283
|
+
profile.name = profileForm.name || profile.id;
|
|
2284
|
+
profile.enabled = !!profileForm.enabled;
|
|
2285
|
+
profile.implementation = profileForm.implementation || profile.id;
|
|
2286
|
+
profile.baseUrl = profileForm.baseUrl || "";
|
|
2287
|
+
profile.apiKey = profileForm.apiKey || "";
|
|
2288
|
+
profile.apiKeyEnv = profileForm.apiKeyEnv || "";
|
|
2289
|
+
profile.model = profileForm.model || "";
|
|
2290
|
+
profile.version = profileForm.version || "2023-06-01";
|
|
2291
|
+
profile.temperature = Number(profileForm.temperature || 0);
|
|
2292
|
+
profile.maxTokens = Number(profileForm.maxTokens || 4096);
|
|
2293
|
+
profile.timeoutMs = Number(profileForm.timeoutMs || 300000);
|
|
2294
|
+
aiProfilesText.value = JSON.stringify(aiConfig, null, 2);
|
|
2295
|
+
return profile;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
function assertDefaultProfileEnabled() {
|
|
2299
|
+
const defaultProfile = aiConfig.profiles.find(p => p.id === aiConfig.defaultProfileId);
|
|
2300
|
+
if (defaultProfile && defaultProfile.enabled === false) {
|
|
2301
|
+
throw new Error(t("defaultProfileDisabledError"));
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
async function saveCurrentProfile() {
|
|
2306
|
+
try {
|
|
2307
|
+
syncProfileFormToConfig();
|
|
2308
|
+
assertDefaultProfileEnabled();
|
|
2309
|
+
await saveAiProfiles();
|
|
2310
|
+
} catch (e) {
|
|
2311
|
+
aiMessage.value = `Save failed: ${e.message}`;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
async function addProfile() {
|
|
2316
|
+
const rawId = window.prompt(t("profileIdPrompt"));
|
|
2317
|
+
const id = normalizeProfileId(rawId);
|
|
2318
|
+
if (!id) return;
|
|
2319
|
+
if (aiConfig.profiles.find(p => p.id === id)) {
|
|
2320
|
+
aiMessage.value = t("duplicateProfileError");
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const realAdapter = adapters.value.find(a => a.id === "claude-code-agent") || adapters.value[0] || { id: "mock-agent" };
|
|
2324
|
+
aiConfig.profiles.push({
|
|
2325
|
+
id,
|
|
2326
|
+
name: id,
|
|
2327
|
+
enabled: false,
|
|
2328
|
+
implementation: realAdapter.id,
|
|
2329
|
+
baseUrl: realAdapter.id === "claude-code-agent" ? "https://api.anthropic.com" : "",
|
|
2330
|
+
apiKey: "",
|
|
2331
|
+
apiKeyEnv: "",
|
|
2332
|
+
model: "",
|
|
2333
|
+
version: "2023-06-01",
|
|
2334
|
+
temperature: 0.2,
|
|
2335
|
+
maxTokens: 4096,
|
|
2336
|
+
timeoutMs: 300000,
|
|
2337
|
+
});
|
|
2338
|
+
selectedConfigProfileId.value = id;
|
|
2339
|
+
loadProfileForm();
|
|
2340
|
+
syncProfileFormToConfig();
|
|
2341
|
+
aiMessage.value = "";
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
async function deleteCurrentProfile() {
|
|
2345
|
+
const id = selectedConfigProfileId.value;
|
|
2346
|
+
if (!id) return;
|
|
2347
|
+
if (id === aiConfig.defaultProfileId) {
|
|
2348
|
+
aiMessage.value = t("deleteDefaultProfileError");
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (!window.confirm(t("deleteProfileConfirm"))) return;
|
|
2352
|
+
const index = aiConfig.profiles.findIndex(p => p.id === id);
|
|
2353
|
+
if (index === -1) return;
|
|
2354
|
+
aiConfig.profiles.splice(index, 1);
|
|
2355
|
+
selectedConfigProfileId.value = aiConfig.defaultProfileId || (aiConfig.profiles[0] && aiConfig.profiles[0].id) || "";
|
|
2356
|
+
loadProfileForm();
|
|
2357
|
+
aiProfilesText.value = JSON.stringify(aiConfig, null, 2);
|
|
2358
|
+
await saveAiProfiles();
|
|
2359
|
+
}
|
|
2360
|
+
|
|
946
2361
|
async function saveAiProfiles() {
|
|
947
2362
|
try {
|
|
948
2363
|
const parsed = JSON.parse(aiProfilesText.value);
|
|
@@ -954,6 +2369,35 @@ createApp({
|
|
|
954
2369
|
}
|
|
955
2370
|
}
|
|
956
2371
|
|
|
2372
|
+
async function testCurrentProfile() {
|
|
2373
|
+
testingProfile.value = true;
|
|
2374
|
+
testResult.value = "";
|
|
2375
|
+
aiMessage.value = "";
|
|
2376
|
+
try {
|
|
2377
|
+
syncProfileFormToConfig();
|
|
2378
|
+
assertDefaultProfileEnabled();
|
|
2379
|
+
await api("PUT", "/api/ai-profiles", JSON.parse(aiProfilesText.value));
|
|
2380
|
+
const data = await api("POST", "/api/ai-profiles/test", {
|
|
2381
|
+
profileId: selectedConfigProfileId.value,
|
|
2382
|
+
prompt: testPrompt.value || "what model are you?"
|
|
2383
|
+
});
|
|
2384
|
+
testResult.value = JSON.stringify({
|
|
2385
|
+
ok: data.ok,
|
|
2386
|
+
profileId: data.profileId,
|
|
2387
|
+
model: data.model,
|
|
2388
|
+
baseUrl: data.baseUrl,
|
|
2389
|
+
text: data.text,
|
|
2390
|
+
usage: data.usage
|
|
2391
|
+
}, null, 2);
|
|
2392
|
+
aiMessage.value = "Model test completed.";
|
|
2393
|
+
} catch (e) {
|
|
2394
|
+
testResult.value = JSON.stringify({ ok: false, error: e.message }, null, 2);
|
|
2395
|
+
aiMessage.value = `Model test failed: ${e.message}`;
|
|
2396
|
+
} finally {
|
|
2397
|
+
testingProfile.value = false;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
957
2401
|
async function setProjectAiProfile(slug, aiProfileId) {
|
|
958
2402
|
await api("PUT", `/api/projects/${slug}/ai-profile`, { aiProfileId });
|
|
959
2403
|
await refreshAll();
|
|
@@ -990,6 +2434,75 @@ createApp({
|
|
|
990
2434
|
await refreshAll();
|
|
991
2435
|
}
|
|
992
2436
|
|
|
2437
|
+
async function migrateV3(slug) {
|
|
2438
|
+
await api("POST", `/api/projects/${slug}/migrate-v3`);
|
|
2439
|
+
await refreshAll();
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
async function openRemoveProject(slug) {
|
|
2443
|
+
removeDialog.open = true;
|
|
2444
|
+
removeDialog.slug = slug;
|
|
2445
|
+
removeDialog.preview = {};
|
|
2446
|
+
removeDialog.deleteKb = false;
|
|
2447
|
+
removeDialog.reason = "";
|
|
2448
|
+
removeDialog.confirmText = "";
|
|
2449
|
+
removeDialog.error = "";
|
|
2450
|
+
try {
|
|
2451
|
+
const data = await api("GET", `/api/projects/${slug}/remove-preview`);
|
|
2452
|
+
removeDialog.preview = data.preview || {};
|
|
2453
|
+
} catch (e) {
|
|
2454
|
+
removeDialog.error = e.message;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
function closeRemoveProject() {
|
|
2459
|
+
removeDialog.open = false;
|
|
2460
|
+
removeDialog.slug = "";
|
|
2461
|
+
removeDialog.preview = {};
|
|
2462
|
+
removeDialog.deleteKb = false;
|
|
2463
|
+
removeDialog.reason = "";
|
|
2464
|
+
removeDialog.confirmText = "";
|
|
2465
|
+
removeDialog.error = "";
|
|
2466
|
+
removeDialog.busy = false;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
async function confirmRemoveProject() {
|
|
2470
|
+
if (removeProjectDisabled.value) return;
|
|
2471
|
+
removeDialog.busy = true;
|
|
2472
|
+
removeDialog.error = "";
|
|
2473
|
+
try {
|
|
2474
|
+
await api("POST", `/api/projects/${removeDialog.slug}/remove`, {
|
|
2475
|
+
deleteKb: removeDialog.deleteKb,
|
|
2476
|
+
reason: removeDialog.reason,
|
|
2477
|
+
});
|
|
2478
|
+
const removed = removeDialog.slug;
|
|
2479
|
+
closeRemoveProject();
|
|
2480
|
+
if (selectedSlug.value === removed) selectedSlug.value = "";
|
|
2481
|
+
await refreshAll();
|
|
2482
|
+
} catch (e) {
|
|
2483
|
+
removeDialog.error = e.message;
|
|
2484
|
+
} finally {
|
|
2485
|
+
removeDialog.busy = false;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
async function runKnowledgeUpdate(slug) {
|
|
2490
|
+
if (!slug) return;
|
|
2491
|
+
actionMessage.value = t("knowledgeUpdateRunning");
|
|
2492
|
+
try {
|
|
2493
|
+
const data = await api("POST", `/api/projects/${slug}/knowledge-update`, {});
|
|
2494
|
+
actionMessage.value = data.reviewRequired
|
|
2495
|
+
? `${t("knowledgeUpdateReviewRequired")}: ${data.reviewReason || ""}`
|
|
2496
|
+
: t("knowledgeUpdateDone");
|
|
2497
|
+
await refreshAll();
|
|
2498
|
+
if (data.reviewRequired) {
|
|
2499
|
+
await loadRuns(slug);
|
|
2500
|
+
}
|
|
2501
|
+
} catch (e) {
|
|
2502
|
+
actionMessage.value = e.message;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
993
2506
|
async function initProject(slug) {
|
|
994
2507
|
busySlug.value = slug;
|
|
995
2508
|
try {
|
|
@@ -1028,9 +2541,25 @@ createApp({
|
|
|
1028
2541
|
}
|
|
1029
2542
|
|
|
1030
2543
|
function resetForm() {
|
|
1031
|
-
Object.assign(form, { slug: "", displayName: "", localPath: "", gitPath: "", primaryLanguage: "", tagsStr: "", isReference: false, initNow: true, knowledgeLanguage: "zh-CN" });
|
|
2544
|
+
Object.assign(form, { slug: "", displayName: "", localPath: "", gitPath: "", primaryLanguage: "", tagsStr: "", isReference: false, initNow: true, initGit: false, createInitialCommit: false, remoteUrl: "", knowledgeLanguage: "zh-CN" });
|
|
1032
2545
|
formError.value = "";
|
|
1033
2546
|
formOk.value = "";
|
|
2547
|
+
importPreflight.value = null;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
async function runImportPreflight() {
|
|
2551
|
+
formError.value = "";
|
|
2552
|
+
importPreflight.value = null;
|
|
2553
|
+
try {
|
|
2554
|
+
const data = await api("POST", "/api/projects/import-preflight", {
|
|
2555
|
+
localPath: form.localPath,
|
|
2556
|
+
gitPath: form.gitPath || form.localPath,
|
|
2557
|
+
});
|
|
2558
|
+
importPreflight.value = data;
|
|
2559
|
+
if (data.needsGitInit) form.initGit = true;
|
|
2560
|
+
} catch (e) {
|
|
2561
|
+
formError.value = e.message;
|
|
2562
|
+
}
|
|
1034
2563
|
}
|
|
1035
2564
|
|
|
1036
2565
|
async function addProject() {
|
|
@@ -1054,7 +2583,15 @@ createApp({
|
|
|
1054
2583
|
kbSchemaVersion: "v1",
|
|
1055
2584
|
goalStatus: "not-created"
|
|
1056
2585
|
};
|
|
1057
|
-
await api("PUT", "/api/projects", {
|
|
2586
|
+
await api("PUT", "/api/projects", {
|
|
2587
|
+
slug: form.slug,
|
|
2588
|
+
config,
|
|
2589
|
+
importOptions: {
|
|
2590
|
+
initGit: !!form.initGit,
|
|
2591
|
+
createInitialCommit: !!form.createInitialCommit,
|
|
2592
|
+
remoteUrl: form.remoteUrl || "",
|
|
2593
|
+
},
|
|
2594
|
+
});
|
|
1058
2595
|
if (form.initNow) await api("POST", `/api/projects/${form.slug}/init`);
|
|
1059
2596
|
formOk.value = `Imported ${form.slug}.`;
|
|
1060
2597
|
await refreshAll();
|
|
@@ -1086,6 +2623,7 @@ createApp({
|
|
|
1086
2623
|
const data = await api("GET", `/api/projects/${slug}/runs/${runId}`);
|
|
1087
2624
|
selectedRun.value = data.run;
|
|
1088
2625
|
drafts.value = data.drafts || [];
|
|
2626
|
+
if (draftBranchFilter.value && !draftBranches.value.includes(draftBranchFilter.value)) draftBranchFilter.value = "";
|
|
1089
2627
|
for (const d of drafts.value) {
|
|
1090
2628
|
if (!(d.path in draftSelection)) draftSelection[d.path] = true;
|
|
1091
2629
|
}
|
|
@@ -1100,12 +2638,12 @@ createApp({
|
|
|
1100
2638
|
|
|
1101
2639
|
async function applySelectedDrafts() {
|
|
1102
2640
|
if (!selectedSlug.value || !selectedRunId.value) return;
|
|
1103
|
-
const selected =
|
|
2641
|
+
const selected = filteredDrafts.value.filter(d => draftSelection[d.path]);
|
|
1104
2642
|
if (!selected.length) throw new Error("No selected drafts.");
|
|
1105
2643
|
const draftPayload = [];
|
|
1106
2644
|
for (const d of selected) {
|
|
1107
2645
|
const raw = await api("GET", `/api/projects/${selectedSlug.value}/drafts/${selectedRunId.value}/raw?path=${encodeURIComponent(d.path)}`);
|
|
1108
|
-
draftPayload.push({ path: d.path, content: raw.content || "" });
|
|
2646
|
+
draftPayload.push({ path: d.path, content: raw.content || "", sourceBranch: d.sourceBranch, sourceHeadCommit: d.sourceHeadCommit });
|
|
1109
2647
|
}
|
|
1110
2648
|
await api("POST", `/api/projects/${selectedSlug.value}/drafts/${selectedRunId.value}/apply`, {
|
|
1111
2649
|
drafts: draftPayload,
|
|
@@ -1147,17 +2685,29 @@ createApp({
|
|
|
1147
2685
|
|
|
1148
2686
|
return {
|
|
1149
2687
|
kbRoot, projects, schedule, lastRun, runningJobs, jobHistory, selectedJob, logOutput, logTitle,
|
|
2688
|
+
summaryPanel, pendingCommitItems, supervisionIssues, issuesByProject, structuredLogs, selectedLog, logFilters,
|
|
2689
|
+
knowledgeStoreConfig, knowledgeMigrationPlan, loggingConfig,
|
|
1150
2690
|
loading, pollError, activeView, selectedSlug, selectedProject, pageTitle, projectList, summaryCards, navItems,
|
|
1151
2691
|
theme, uiLanguage, t, toggleTheme, form, submitting, formError, formOk, addProject, resetForm, busySlug, initProject,
|
|
1152
2692
|
freq, scheduleMsg, applySchedule, deleteSchedule,
|
|
1153
|
-
runs, selectedRunId, selectedRun, drafts, draftSelection, previewDraftPath, draftPreview, allowGoalEdit,
|
|
1154
|
-
|
|
1155
|
-
|
|
2693
|
+
runs, selectedRunId, selectedRun, drafts, filteredDrafts, draftBranches, draftBranchFilter, applyDraftButtonLabel, draftSelection, previewDraftPath, draftPreview, allowGoalEdit,
|
|
2694
|
+
removeDialog, removeProjectDisabled,
|
|
2695
|
+
aiConfig, aiProfilesText, adapters, aiMessage, selectedConfigProfileId, profileForm, testPrompt, testResult, testingProfile, selectedAiProfileId, selectedKnowledgeLanguage,
|
|
2696
|
+
hookStatus, actionMessage, advancedOpen, importPreflight,
|
|
1156
2697
|
selectProject, isKbInit, isProjectRunning, repoStatusLevel, projectStatusClass, projectIssues,
|
|
1157
|
-
kbUrl, goalUrl, displayProjectKbPath, formatTime,
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
2698
|
+
kbUrl, goalUrl, displayProjectKbPath, formatTime, formatBytes,
|
|
2699
|
+
profileOptionLabel,
|
|
2700
|
+
refreshAll, loadJobs, loadJobDetail, loadAiProfiles, loadProfileForm, addProfile, deleteCurrentProfile, saveAiProfiles, saveCurrentProfile, testCurrentProfile, setProjectAiProfile, saveProjectSettings,
|
|
2701
|
+
openSummaryPanel, loadPendingCommits, loadIssues, loadStructuredLogs,
|
|
2702
|
+
loadKnowledgeStoreConfig, saveKnowledgeStoreConfig, previewKnowledgeMigration, executeKnowledgeMigration, loadLoggingConfig, saveLoggingConfig,
|
|
2703
|
+
validateGit, scanProject, validateKb, migrateV2, migrateV3, runKnowledgeUpdate, runImportPreflight, runJob, loadHookStatus, installHook, uninstallHook,
|
|
2704
|
+
openRemoveProject, closeRemoveProject, confirmRemoveProject,
|
|
2705
|
+
loadRuns, selectRun, loadRunDetail, loadDraftPreview, applySelectedDrafts, rejectRun,
|
|
2706
|
+
terminalLines, terminalSessionId, terminalSession, terminalSessions, terminalPermission, terminalPermissionBusy, terminalInput, terminalScrollRef,
|
|
2707
|
+
terminalStatusDot, terminalInputPlaceholder, permissionSummaryText, renderMarkdown,
|
|
2708
|
+
startClaudeAnalysis, sendClaudeInput, stopClaudeSession, clearTerminal, restoreLatestClaudeSession, approveClaudePermission, denyClaudePermission,
|
|
2709
|
+
promptsConfig, promptsSaving, promptsError, promptsSaved,
|
|
2710
|
+
loadPrompts, savePrompts
|
|
1161
2711
|
};
|
|
1162
2712
|
}
|
|
1163
2713
|
}).mount("#app");
|