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.
Files changed (39) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +201 -58
  3. package/_site/_test/ai-profile-test.js +59 -1
  4. package/_site/_test/baseline-schema-test.js +4 -3
  5. package/_site/_test/claude-workbench-test.js +72 -0
  6. package/_site/_test/draft-apply-test.js +12 -6
  7. package/_site/_test/kb-v2-templates-test.js +31 -43
  8. package/_site/_test/knowledge-store-logs-supervision-test.js +143 -0
  9. package/_site/_test/package-startup-test.js +108 -0
  10. package/_site/_test/project-control-panel-task14-test.js +151 -0
  11. package/_site/_test/task15-20-integration-test.js +194 -0
  12. package/_site/_test/task15-20-ui-flow-test.js +144 -0
  13. package/_site/_test/ui-smoke-test.js +2 -2
  14. package/_site/index.html +1640 -90
  15. package/_site/lib/ai-adapter.js +3 -3
  16. package/_site/lib/ai-workspace.js +120 -0
  17. package/_site/lib/analysis-orchestrator.js +117 -32
  18. package/_site/lib/claude-cli-runner.js +862 -0
  19. package/_site/lib/context-pack-builder.js +19 -11
  20. package/_site/lib/draft-apply.js +80 -31
  21. package/_site/lib/index-builder.js +100 -0
  22. package/_site/lib/job-orchestrator.js +15 -11
  23. package/_site/lib/kb-v3.js +188 -0
  24. package/_site/lib/kb-validator.js +84 -0
  25. package/_site/lib/knowledge-store.js +141 -0
  26. package/_site/lib/llm-client.js +103 -56
  27. package/_site/lib/prompt-registry.js +102 -0
  28. package/_site/lib/structured-logger.js +120 -0
  29. package/_site/lib/supervision.js +103 -0
  30. package/_site/server.js +887 -30
  31. package/_site/vendor/tailwind-browser.js +947 -0
  32. package/_site/vendor/vue.global.prod.js +9 -0
  33. package/ai-profiles.json +13 -3
  34. package/bin/project-knowledge.js +51 -0
  35. package/docs/development-progress.md +141 -0
  36. package/package.json +11 -2
  37. package/scripts/gen-commit-doc.ps1 +1 -1
  38. package/scripts/list-features.ps1 +1 -1
  39. 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="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
8
- <script src="https://cdn.tailwindcss.com"></script>
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
- <div v-for="s in summaryCards" :key="s.label" class="panel rounded-xl border p-4">
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
- </div>
137
+ </button>
122
138
  </div>
123
139
 
124
- <div class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_390px]">
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="validateGit(selectedSlug)" class="btn">{{ t("validateGit") }}</button>
139
- <button @click="scanProject(selectedSlug)" class="btn">{{ t("scan") }}</button>
140
- <button @click="runJob('safe', selectedSlug)" class="btn-primary">{{ t("safeAnalyze") }}</button>
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 md:grid-cols-2">
158
- <button @click="initProject(selectedSlug)" class="btn">{{ t("initKb") }}</button>
159
- <button @click="migrateV2(selectedSlug)" class="btn">{{ t("migrateV2") }}</button>
160
- <button @click="runJob('scan', selectedSlug)" class="btn">{{ t("runScanJob") }}</button>
161
- <button @click="runJob('analyze-initial', selectedSlug)" class="btn">{{ t("initialAnalysis") }}</button>
162
- <button @click="runJob('analyze-commits', selectedSlug)" class="btn">{{ t("commitAnalysis") }}</button>
163
- <button @click="validateKb(selectedSlug)" class="btn">{{ t("validateKb") }}</button>
164
- <a v-if="isKbInit(selectedSlug)" :href="kbUrl(selectedSlug)" target="_blank" class="btn text-center">{{ t("openKb") }}</a>
165
- <a :href="goalUrl(selectedSlug)" target="_blank" class="btn text-center">{{ t("openGoal") }}</a>
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-5">
213
- <div class="mb-4 flex items-center justify-between">
214
- <h2 class="font-semibold">{{ t("runningJobs") }}</h2>
215
- <span class="chip">{{ runningJobs.length }}</span>
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="runningJobs.length" class="space-y-3">
218
- <div v-for="job in runningJobs" :key="job.jobId" class="panel2 rounded-xl border p-3">
219
- <div class="flex items-center justify-between gap-2">
220
- <div class="truncate text-sm font-medium">{{ job.mode || "job" }} / {{ job.slug }}</div>
221
- <span class="dot warn"></span>
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
- <div class="mt-2 h-1.5 overflow-hidden rounded-full bg-slate-500/20">
224
- <div class="h-full w-2/3 rounded-full" style="background: var(--warn)"></div>
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="mt-2 text-xs muted">{{ job.status }} / {{ formatTime(job.startTime) }}</div>
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 v-else class="rounded-lg border px-3 py-6 text-center text-sm panel2 muted">{{ t("noRunningJob") }}</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="saveAiProfiles" class="btn-primary">{{ t("save") }}</button>
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">{{ t("defaultProfile") }}
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">{{ p.id }} - {{ p.name || p.id }}</option>
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="16" class="input font-mono text-xs"></textarea>
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">{{ t("defaultProfile") }}
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.id }}</option>
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">{{ t("applySelected") }}</button>
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
- <span class="muted">{{ drafts.length }} {{ t("draftFiles") }}</span>
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 drafts" :key="d.path" class="flex cursor-pointer items-center justify-between gap-3 rounded-lg border p-3 text-sm panel2">
381
- <span class="truncate">{{ d.path }}</span>
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 drafts" :key="d.path" :value="d.path">{{ d.path }}</option>
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 xl:grid-cols-[360px_minmax(0,1fr)]">
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-center justify-between">
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("logOutput") }}</h2>
460
- <div class="mt-1 text-sm muted">{{ logTitle }}</div>
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
- <pre class="max-h-[72vh] overflow-auto rounded-xl bg-slate-950 p-4 text-xs text-slate-100 scrollbar-thin whitespace-pre-wrap">{{ logOutput || t("noOutput") }}</pre>
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 project-goal.md edit",
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: "允许修改 project-goal.md",
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(kbRoot.value || "D:\\SanQian.Xu\\project-knowledge-base");
813
- return `${root}\\projects\\${slug}`;
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(kbRoot.value || "project-knowledge-base")}\\projects\\${slug}`;
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
- return fileUrlFromPath(`${(cfg && cfg.kbPath) || projectKbPath(slug)}\\project-goal.md`);
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, '&amp;')
1805
+ .replace(/</g, '&lt;')
1806
+ .replace(/>/g, '&gt;')
1807
+ .replace(/"/g, '&quot;')
1808
+ .replace(/'/g, '&#39;');
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", { slug: form.slug, config });
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 = drafts.value.filter(d => draftSelection[d.path]);
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
- aiConfig, aiProfilesText, adapters, aiMessage, selectedAiProfileId, selectedKnowledgeLanguage,
1155
- hookStatus, actionMessage,
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
- refreshAll, loadJobs, loadJobDetail, loadAiProfiles, saveAiProfiles, setProjectAiProfile, saveProjectSettings,
1159
- validateGit, scanProject, validateKb, migrateV2, runJob, loadHookStatus, installHook, uninstallHook,
1160
- loadRuns, selectRun, loadRunDetail, loadDraftPreview, applySelectedDrafts, rejectRun
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");