dsh-knowledge-library 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +133 -14
  3. package/dist/client.js +1654 -304
  4. package/dist/core/extract.d.ts +5 -0
  5. package/dist/core/extract.js +78 -8
  6. package/dist/core/fetch.d.ts +5 -0
  7. package/dist/core/fetch.js +102 -25
  8. package/dist/core/grouping.d.ts +6 -0
  9. package/dist/core/grouping.js +71 -0
  10. package/dist/core/image-capture.d.ts +33 -0
  11. package/dist/core/image-capture.js +196 -0
  12. package/dist/core/incremental.d.ts +14 -0
  13. package/dist/core/incremental.js +52 -0
  14. package/dist/core/jobs.d.ts +21 -1
  15. package/dist/core/jobs.js +101 -12
  16. package/dist/core/knowledge.d.ts +37 -0
  17. package/dist/core/knowledge.js +619 -0
  18. package/dist/core/library.d.ts +36 -2
  19. package/dist/core/library.js +363 -17
  20. package/dist/core/page-access.d.ts +6 -0
  21. package/dist/core/page-access.js +38 -0
  22. package/dist/core/page-images.d.ts +15 -0
  23. package/dist/core/page-images.js +97 -0
  24. package/dist/core/purge.d.ts +20 -0
  25. package/dist/core/purge.js +106 -0
  26. package/dist/directory-settings.d.ts +18 -0
  27. package/dist/directory-settings.js +92 -0
  28. package/dist/http.d.ts +3 -1
  29. package/dist/http.js +152 -4
  30. package/dist/index.js +22 -7
  31. package/dist/knowledge-model.d.ts +30 -0
  32. package/dist/knowledge-model.js +194 -0
  33. package/dist/preview-settings.d.ts +6 -0
  34. package/dist/preview-settings.js +42 -0
  35. package/dist/public/app.css +145 -2
  36. package/dist/public/app.js +10 -6
  37. package/dist/standalone.js +12 -4
  38. package/dist/tools.d.ts +2 -1
  39. package/dist/tools.js +10 -1
  40. package/dist/types.d.ts +199 -1
  41. package/dist/ui/App.js +248 -47
  42. package/dist/ui/Dialog.d.ts +7 -0
  43. package/dist/ui/Dialog.js +32 -0
  44. package/dist/ui/Grouping.d.ts +12 -0
  45. package/dist/ui/Grouping.js +93 -0
  46. package/dist/ui/ImageCapture.d.ts +18 -0
  47. package/dist/ui/ImageCapture.js +76 -0
  48. package/dist/ui/Knowledge.d.ts +26 -0
  49. package/dist/ui/Knowledge.js +106 -0
  50. package/dist/ui/api.d.ts +7 -0
  51. package/dist/ui/api.js +16 -0
  52. package/dist/ui/icons.js +2 -0
  53. package/package.json +5 -1
@@ -0,0 +1,194 @@
1
+ const unavailable = '请先在 Harness 设置中配置默认模型;独立预览可通过环境变量配置模型。';
2
+ class ModelResponseError extends Error {
3
+ }
4
+ function outputLimitMessage() {
5
+ return '模型输出达到当前模型或服务的长度上限。插件沿用模型默认输出配置,未另设 token 上限;思考可能与正文共用额度,本次未自动重试。请检查模型配置,或分批整理后主动重试。';
6
+ }
7
+ async function boundedJson(response, limit = 512_000) {
8
+ const reader = response.body?.getReader();
9
+ if (!reader)
10
+ throw new Error('模型响应为空');
11
+ const chunks = [];
12
+ let length = 0;
13
+ try {
14
+ for (;;) {
15
+ const part = await reader.read();
16
+ if (part.done)
17
+ break;
18
+ length += part.value.length;
19
+ if (length > limit)
20
+ throw new Error('模型响应过大,请缩小整理范围');
21
+ chunks.push(part.value);
22
+ }
23
+ }
24
+ finally {
25
+ await reader.cancel().catch(() => { });
26
+ }
27
+ try {
28
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
29
+ }
30
+ catch {
31
+ throw new Error('模型响应不是有效 JSON');
32
+ }
33
+ }
34
+ function checkImages(images) {
35
+ if (!images?.length)
36
+ return;
37
+ if (images.length > 10 || images.some(image => !image.data.byteLength || image.data.byteLength > 6 * 1024 * 1024 || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.mediaType)) || images.reduce((sum, image) => sum + image.data.byteLength, 0) > 20 * 1024 * 1024)
38
+ throw new Error('图片请求超过数量、格式或字节限制');
39
+ }
40
+ export function harnessKnowledgeModel(ctx) {
41
+ function services() {
42
+ const llm = ctx.get('llm');
43
+ const defaults = ctx.get('agentDefaultModel');
44
+ const selection = defaults?.currentSelection();
45
+ if (!llm || !selection?.provider || !selection.model || !llm.listProviders().some(p => p.id === selection.provider))
46
+ throw new Error(unavailable);
47
+ return { llm, selection };
48
+ }
49
+ async function visionStatus() {
50
+ try {
51
+ const { llm, selection } = services();
52
+ const model = `${selection.provider} / ${selection.model}`;
53
+ if (!ctx.get('attachments'))
54
+ return { available: false, model, reason: '当前宿主未提供图片附件服务,请升级或启用附件服务' };
55
+ const info = await llm.resolveModelInfo(selection.provider, selection.model, AbortSignal.timeout(5000));
56
+ return info.inputModalities?.includes('image') ? { available: true, model } : { available: false, model, reason: '当前默认模型未声明图片输入能力,请在 Harness 中选择支持视觉的模型(如 deepseek-flash)' };
57
+ }
58
+ catch {
59
+ return { available: false, reason: '无法确认默认模型的图片能力,请检查 Harness 模型设置' };
60
+ }
61
+ }
62
+ return {
63
+ visionStatus,
64
+ status() {
65
+ try {
66
+ const { selection } = services();
67
+ return { available: true, model: `${selection.provider} / ${selection.model}` };
68
+ }
69
+ catch {
70
+ return { available: false, reason: unavailable };
71
+ }
72
+ },
73
+ async generate({ system, prompt, signal, images, expectedModel }) {
74
+ checkImages(images);
75
+ const { llm, selection } = services();
76
+ if (expectedModel !== undefined && expectedModel !== `${selection.provider} / ${selection.model}`)
77
+ throw new Error('模型已改变,请重新核对');
78
+ signal.throwIfAborted();
79
+ const content = [{ type: 'text', text: prompt }];
80
+ if (images?.length) {
81
+ const status = await visionStatus();
82
+ signal.throwIfAborted();
83
+ if (!status.available || status.model !== `${selection.provider} / ${selection.model}`)
84
+ throw new Error(status.reason || '图片模型配置已改变');
85
+ const attachments = ctx.get('attachments');
86
+ const refs = await attachments.saveImages(images);
87
+ signal.throwIfAborted();
88
+ content.push(...refs.map(attachment => ({ type: 'image', attachment })));
89
+ }
90
+ const current = services().selection;
91
+ if (current.provider !== selection.provider || current.model !== selection.model || current.reasoningEffort !== selection.reasoningEffort)
92
+ throw new Error('模型配置已改变,请重新核对');
93
+ let text = '';
94
+ let finished = false;
95
+ // Omit maxTokens so the Harness runtime materializes the model's own defaults.
96
+ try {
97
+ for await (const chunk of llm.stream({ ...selection, system, messages: [{ role: 'user', content }], signal })) {
98
+ signal.throwIfAborted();
99
+ // Complete blocks are authoritative; deltas may include reasoning or retries.
100
+ if (chunk.type === 'block-end' && chunk.block.type === 'text')
101
+ text += chunk.block.text;
102
+ if (text.length > 60_000)
103
+ throw new ModelResponseError('模型输出过长,请缩小整理范围');
104
+ if (chunk.type === 'finish') {
105
+ if (chunk.reason.kind !== 'stop')
106
+ throw new ModelResponseError(chunk.reason.kind === 'max-tokens' ? outputLimitMessage() : '模型调用未完成,请检查 Harness 模型设置后重试');
107
+ finished = true;
108
+ }
109
+ }
110
+ }
111
+ catch (error) {
112
+ signal.throwIfAborted();
113
+ if (error instanceof ModelResponseError)
114
+ throw error;
115
+ throw new Error('模型调用失败,请检查 Harness 模型配置或网络后重试');
116
+ }
117
+ signal.throwIfAborted();
118
+ if (!finished || !text.trim())
119
+ throw new Error('模型没有返回完整正文,请重试');
120
+ return text;
121
+ },
122
+ };
123
+ }
124
+ /** Preview credentials stay on the server and are never returned through HTTP. */
125
+ export function previewKnowledgeModel(env = process.env) {
126
+ const key = env.KNOWLEDGE_LIBRARY_MODEL_API_KEY || env.DEEPSEEK_API_KEY;
127
+ const model = env.KNOWLEDGE_LIBRARY_MODEL || 'deepseek-chat';
128
+ const base = env.KNOWLEDGE_LIBRARY_MODEL_BASE_URL || 'https://api.deepseek.com';
129
+ let cached;
130
+ async function visionStatus() {
131
+ if (!key)
132
+ return { available: false, model, reason: unavailable };
133
+ if (cached && Date.now() - cached.time < 15_000)
134
+ return cached.value;
135
+ let value;
136
+ try {
137
+ const response = await fetch(`${base.replace(/\/$/, '')}/models`, { headers: { Authorization: `Bearer ${key}` }, redirect: 'error', signal: AbortSignal.timeout(5000) });
138
+ if (!response.ok) {
139
+ await response.body?.cancel();
140
+ throw new Error();
141
+ }
142
+ const data = await boundedJson(response);
143
+ const entry = Array.isArray(data.data) && data.data.find((item) => item.id === model);
144
+ value = entry?.input_modalities?.includes('image') ? { available: true, model } : { available: false, model, reason: '当前模型未声明图片输入能力,请设置支持视觉的 KNOWLEDGE_LIBRARY_MODEL(如 deepseek-flash)' };
145
+ }
146
+ catch {
147
+ value = { available: false, model, reason: '无法读取模型图片能力,请检查服务地址的 /models 接口和网络' };
148
+ }
149
+ cached = { time: Date.now(), value };
150
+ return value;
151
+ }
152
+ return {
153
+ visionStatus,
154
+ status: () => key ? { available: true, model } : { available: false, reason: unavailable },
155
+ async generate({ system, prompt, signal, images, expectedModel }) {
156
+ checkImages(images);
157
+ if (!key)
158
+ throw new Error(unavailable);
159
+ if (expectedModel !== undefined && expectedModel !== model)
160
+ throw new Error('模型已改变,请重新核对');
161
+ signal.throwIfAborted();
162
+ if (images?.length) {
163
+ const info = await visionStatus();
164
+ signal.throwIfAborted();
165
+ if (!info.available)
166
+ throw new Error(info.reason);
167
+ }
168
+ let response;
169
+ try {
170
+ const url = new URL(`${base.replace(/\/$/, '')}/chat/completions`);
171
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password)
172
+ throw new Error();
173
+ response = await fetch(url, { method: 'POST', redirect: 'error', signal,
174
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
175
+ body: JSON.stringify({ model, stream: false, messages: [{ role: 'system', content: system }, { role: 'user', content: images?.length ? [{ type: 'text', text: prompt }, ...images.map(image => ({ type: 'image_url', image_url: { url: `data:${image.mediaType};base64,${Buffer.from(image.data).toString('base64')}`, detail: 'original' } }))] : prompt }] }) });
176
+ }
177
+ catch {
178
+ signal.throwIfAborted();
179
+ throw new Error('模型连接失败,请检查服务地址和网络后重试');
180
+ }
181
+ if (!response.ok) {
182
+ await response.body?.cancel();
183
+ throw new Error(`模型调用失败(HTTP ${response.status}),请检查模型配置或额度`);
184
+ }
185
+ const data = await boundedJson(response);
186
+ const choice = data.choices?.[0];
187
+ if (choice?.finish_reason === 'length' || choice?.finish_reason === 'max_tokens')
188
+ throw new Error(outputLimitMessage());
189
+ if (choice?.finish_reason !== 'stop' || typeof choice.message?.content !== 'string' || !choice.message.content.trim())
190
+ throw new Error('模型未返回完整正文,可能达到输出上限,请缩小范围后重试');
191
+ return choice.message.content;
192
+ },
193
+ };
194
+ }
@@ -0,0 +1,6 @@
1
+ import type { DirectorySettings } from './directory-settings.js';
2
+ import { type Library } from './core/library.js';
3
+ import type { FetchPolicy } from './core/fetch.js';
4
+ export declare function createPreviewSettings(library: Library, defaults?: FetchPolicy): DirectorySettings & {
5
+ fetchPolicy(): FetchPolicy;
6
+ };
@@ -0,0 +1,42 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { atomicJson } from './core/library.js';
4
+ export function createPreviewSettings(library, defaults = {}) {
5
+ const file = join(library.root, '.system', 'preview-settings.json');
6
+ let allowProxyFakeIp = defaults.allowProxyFakeIp === true;
7
+ if (existsSync(file)) {
8
+ let stored;
9
+ try {
10
+ stored = JSON.parse(readFileSync(file, 'utf8'));
11
+ }
12
+ catch {
13
+ throw new Error('独立预览设置读取失败,请检查 .system/preview-settings.json');
14
+ }
15
+ if (stored?.schemaVersion !== 1 || typeof stored.allowProxyFakeIp !== 'boolean')
16
+ throw new Error('独立预览设置格式不正确,请检查 .system/preview-settings.json');
17
+ allowProxyFakeIp = stored.allowProxyFakeIp;
18
+ }
19
+ const unsupported = async () => { throw new Error('独立预览请通过启动配置指定资料库目录'); };
20
+ return {
21
+ status: () => ({ canChange: false, canChangeNetwork: true, picker: null, allowProxyFakeIp }),
22
+ // Each request keeps its own policy even if settings change during a redirect.
23
+ fetchPolicy: () => ({ allowProxyFakeIp }),
24
+ pick: unsupported,
25
+ list: unsupported,
26
+ change: unsupported,
27
+ async setProxyMode(enabled, expected, expectedRoot) {
28
+ if (expectedRoot !== library.root || expected !== allowProxyFakeIp)
29
+ throw new Error('资料库设置已改变,请重新打开设置后重试');
30
+ if (enabled === allowProxyFakeIp)
31
+ return enabled;
32
+ try {
33
+ atomicJson(file, { schemaVersion: 1, allowProxyFakeIp: enabled });
34
+ }
35
+ catch {
36
+ throw new Error('采集设置保存失败,请检查资料库目录的写入权限;原设置未改变');
37
+ }
38
+ allowProxyFakeIp = enabled;
39
+ return enabled;
40
+ },
41
+ };
42
+ }
@@ -91,11 +91,100 @@
91
91
  .kl-filterbar b { font-weight: 400; }
92
92
  .kl-filterbar > div { display: flex; align-items: center; gap: 16px; flex: none; }
93
93
  .kl-filterbar select { background: var(--kl-bg); color: var(--kl-muted); border: 0; border-radius: 6px; padding: 3px; max-width: 110px; font-size: 12px; }
94
+ .kl-trash-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 var(--kl-gutter) 12px; }
95
+ .kl-trash-toolbar p { color: var(--kl-muted); font-size: 12px; margin: 0; min-width: 0; }
96
+ .kl-trash-toolbar button { flex: none; }
97
+ .kl-knowledge-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 var(--kl-gutter) 12px; }
98
+ .kl-knowledge-toolbar p { margin: 0; font-size: 12px; color: var(--kl-muted); }
99
+ .kl-knowledge-toolbar > div, .kl-knowledge-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
100
+ .kl-knowledge-toolbar > div { flex: none; }
101
+ .kl-knowledge-badge { display: inline-flex; align-items: center; padding: 2px 6px; border-radius: 5px; background: var(--kl-hover); color: var(--kl-muted); font-size: 11px; line-height: 18px; white-space: nowrap; font-weight: 400; }
102
+ .kl-knowledge-badge:is(.draft, .running, .queued, .grouping, .classifying) { color: var(--kl-link); }
103
+ .kl-knowledge-badge.failed { color: var(--kl-error); }
104
+ .kl-knowledge-badge.completed { color: var(--kl-ink); }
105
+ .kl-item-bottom { flex-wrap: wrap; }
106
+ .kl-item-row.kl-selectable { padding-left: 30px; }
107
+ .kl-material-checkbox { position: absolute; left: 9px; top: 17px; width: 15px; height: 15px; accent-color: var(--kl-primary); cursor: pointer; }
108
+ .kl-knowledge-source { border-block: .5px solid var(--kl-line); padding-block: 16px; margin: 20px 0; }
109
+ .kl-knowledge-source > div:first-child { display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--kl-muted); margin-bottom: 12px; }
110
+ .kl-knowledge-form, .kl-knowledge-jobs { padding: 0 24px 24px; }
111
+ .kl-knowledge-form label, .kl-knowledge-draft label { display: flex; flex-direction: column; gap: 7px; font-size: 13px; margin: 16px 0; }
112
+ .kl-knowledge-form input, .kl-knowledge-draft input, .kl-knowledge-draft textarea { width: 100%; border: .5px solid var(--kl-line); border-radius: 8px; padding: 10px 12px; color: var(--kl-ink); background: var(--kl-bg); min-width: 0; font: inherit; }
113
+ .kl-knowledge-draft textarea { line-height: 1.8; resize: vertical; font-size: 13px; }
114
+ .kl-knowledge-form small, .kl-knowledge-jobs small { font-size: 12px; color: var(--kl-muted); line-height: 1.7; }
115
+ .kl-knowledge-summary { display: flex; align-items: center; justify-content: space-between; font-size: 13px; }
116
+ .kl-knowledge-summary b { font-weight: 500; }
117
+ .kl-knowledge-summary span { color: var(--kl-muted); }
118
+ .kl-knowledge-source-list { padding: 0; list-style: none; max-height: 150px; overflow: auto; }
119
+ .kl-knowledge-source-list li { display: flex; align-items: center; gap: 8px; margin-block: 8px; font-size: 13px; overflow-wrap: anywhere; }
120
+ .kl-knowledge-model { color: var(--kl-muted); font-size: 12px; line-height: 1.8; }
121
+ .kl-knowledge-help { font-size: 12px; color: var(--kl-link); display: inline-flex; align-items: center; gap: 4px; }
122
+ .kl-knowledge-form > .kl-muted { font-size: 13px; line-height: 1.8; }
123
+ .kl-knowledge-form footer, .kl-knowledge-draft footer { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; margin-top: 20px; }
124
+ .kl-knowledge-back { background: transparent; border: 0; padding: 0; color: var(--kl-muted); font-size: 12px; margin-bottom: 18px; }
125
+ .kl-knowledge-empty { display: flex; align-items: center; flex-direction: column; text-align: center; padding: 24px 0; color: var(--kl-muted); }
126
+ .kl-knowledge-empty p { color: var(--kl-ink); }
127
+ .kl-knowledge-job { display: block; width: 100%; border: 0; border-top: .5px solid var(--kl-line); background: transparent; text-align: left; padding: 16px 0; }
128
+ .kl-knowledge-job:hover { background: var(--kl-hover); }
129
+ .kl-knowledge-job > div, .kl-knowledge-job-heading { display: flex; gap: 12px; justify-content: space-between; align-items: flex-start; }
130
+ .kl-knowledge-job strong, .kl-knowledge-job-heading strong { font-size: 14px; font-weight: 500; overflow-wrap: anywhere; min-width: 0; }
131
+ .kl-knowledge-job p { color: var(--kl-muted); font-size: 12px; margin: 8px 0 0; }
132
+ .kl-knowledge-job small { display: block; margin-top: 8px; }
133
+ .kl-knowledge-progress { display: flex; align-items: center; gap: 8px; color: var(--kl-muted); font-size: 13px; margin: 20px 0; }
134
+ .kl-knowledge-jobs > .kl-knowledge-actions { margin-top: 18px; }
135
+ .kl-knowledge-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
136
+ .kl-knowledge-citations { font-size: 12px; color: var(--kl-muted); border-block: .5px solid var(--kl-line); padding-block: 12px; }
137
+ .kl-knowledge-citations summary { cursor: pointer; }
138
+ .kl-knowledge-citations button { border: 0; background: transparent; display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding: 8px 0; }
139
+ .kl-knowledge-citations button span { min-width: 0; overflow-wrap: anywhere; }
140
+ .kl-knowledge-mode { margin: 16px 0; padding: 12px; border: .5px solid var(--kl-line); border-radius: 8px; }
141
+ .kl-knowledge-mode legend { padding: 0 4px; font-size: 12px; color: var(--kl-muted); }
142
+ .kl-knowledge-mode label { flex-direction: row; align-items: center; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
143
+ .kl-knowledge-mode input { width: 15px; padding: 0; accent-color: var(--kl-primary); }
144
+ .kl-knowledge-mode small { margin-left: auto; }
145
+ .kl-grouping-editor { margin-top: 18px; }
146
+ .kl-grouping-summary { display: flex; align-items: center; gap: 14px; font-size: 13px; }
147
+ .kl-grouping-summary b { font-weight: 500; }
148
+ .kl-grouping-summary span { color: var(--kl-muted); }
149
+ .kl-grouping-group { border: .5px solid var(--kl-line); border-radius: 8px; padding: 12px 14px; margin: 12px 0; }
150
+ .kl-grouping-group-title, .kl-grouping-source-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
151
+ .kl-grouping-group-title { font-size: 12px; color: var(--kl-muted); }
152
+ .kl-grouping-group ul { margin: 10px 0 0; padding-left: 18px; color: var(--kl-muted); font-size: 12px; line-height: 1.8; }
153
+ .kl-grouping-group li { overflow-wrap: anywhere; }
154
+ .kl-grouping-editor label { display: flex; flex-direction: column; gap: 7px; margin: 12px 0; font-size: 12px; }
155
+ .kl-grouping-editor :is(input,textarea,select) { width: 100%; min-width: 0; border: .5px solid var(--kl-line); border-radius: 8px; padding: 9px 10px; background: var(--kl-bg); color: var(--kl-ink); font: inherit; }
156
+ .kl-grouping-editor textarea { line-height: 1.7; resize: vertical; }
157
+ .kl-grouping-reason { margin: 8px 0; font-size: 12px; line-height: 1.8; color: var(--kl-muted); overflow-wrap: anywhere; }
158
+ .kl-grouping-settings summary, .kl-grouping-source summary { cursor: pointer; font-size: 12px; color: var(--kl-muted); }
159
+ .kl-grouping-settings summary span { margin-left: 8px; color: var(--kl-ink); }
160
+ .kl-grouping-section-title { font-size: 14px; font-weight: 500; margin: 24px 0 10px; }
161
+ .kl-grouping-source { border-top: .5px solid var(--kl-line); padding: 14px 0; }
162
+ .kl-grouping-source-title strong { font-size: 13px; font-weight: 500; min-width: 0; overflow-wrap: anywhere; }
163
+ .kl-grouping-source p, .kl-grouping-pending p, .kl-grouping-recovery p { font-size: 12px; line-height: 1.8; color: var(--kl-muted); overflow-wrap: anywhere; }
164
+ .kl-grouping-source details button { margin-top: 8px; }
165
+ .kl-grouping-editor .kl-grouping-sync { flex-direction: row; align-items: center; gap: 8px; margin-top: 20px; }
166
+ .kl-grouping-sync input { width: 15px; height: 15px; padding: 0; accent-color: var(--kl-primary); flex: none; }
167
+ .kl-grouping-editor footer { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 8px; margin-top: 20px; }
168
+ .kl-grouping-recovery { border-left: 2px solid var(--kl-link); padding-left: 12px; font-size: 13px; }
169
+ .kl-grouping-pending { border-top: .5px solid var(--kl-line); padding-top: 14px; font-size: 13px; }
170
+ .kl-note-integration { border: .5px solid var(--kl-line); border-radius: 8px; padding: 14px; margin: 16px 0; }
171
+ .kl-note-integration > strong { font-size: 14px; font-weight: 500; }
172
+ .kl-note-integration p, .kl-integration-findings { font-size: 12px; line-height: 1.8; overflow-wrap: anywhere; }
173
+ .kl-integration-findings { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 12px; padding: 8px 0; border-top: .5px solid var(--kl-line); }
174
+ .kl-integration-findings > span { color: var(--kl-muted); }
175
+ .kl-integration-findings ul { margin: 0; padding-left: 16px; }
176
+ .kl-integration-target { font-weight: 500; }
177
+ .kl-integration-preview { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 300px; overflow-y: auto; padding: 12px; background: var(--kl-hover); border-radius: 6px; font: inherit; font-size: 12px; line-height: 1.8; }
178
+ .kl-note-integration details summary, .kl-knowledge-draft > details summary { cursor: pointer; font-size: 12px; color: var(--kl-muted); }
94
179
  .kl-body { position: relative; display: flex; flex: 1; min-height: 0; overflow: hidden; }
95
180
  .kl-list { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: 0 calc(var(--kl-gutter) - 8px) 24px; }
96
181
  .kl-body.with-detail .kl-list { flex: 0 0 38%; padding: 0 16px 24px calc(var(--kl-gutter) - 8px); }
97
182
  .kl-item { width: 100%; display: block; text-align: left; padding: 14px 12px; margin-bottom: 2px; border: 0; border-radius: var(--kl-radius); background: transparent; }
98
183
  .kl-item:hover, .kl-item.selected { background: var(--kl-hover); }
184
+ .kl-item-row { position: relative; }
185
+ .kl-item-row .kl-item { padding-right: 48px; }
186
+ .kl-app .kl-row-action { position: absolute; top: 8px; right: 8px; }
187
+ .kl-app .kl-danger, .kl-app .kl-danger:hover { color: var(--kl-error); }
99
188
  .kl-item-top { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 5px; }
100
189
  .kl-type { display: inline-flex; gap: 5px; align-items: center; font-size: 12px; color: var(--kl-muted); }
101
190
  .kl-item time { color: var(--kl-muted); font-size: 12px; }
@@ -176,6 +265,9 @@
176
265
  .kl-dialog > header p { margin: 0; color: var(--kl-muted); font-size: 12px; line-height: 1.7; }
177
266
  .kl-dialog > header > button { border: 0; border-radius: 8px; background: transparent; color: var(--kl-muted); padding: 4px; display: flex; }
178
267
  .kl-dialog > header > button:hover { background: var(--kl-hover); }
268
+ .kl-delete-dialog { padding: 0 24px 24px; }
269
+ .kl-delete-title { margin: 0 0 12px; font-weight: 500; overflow-wrap: anywhere; }
270
+ .kl-delete-dialog footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 24px; }
179
271
  .kl-editor { padding: 0 24px; }
180
272
  .kl-tabs { display: flex; gap: 8px; margin-bottom: 20px; }
181
273
  .kl-tabs button { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; background: transparent; border: 0; color: var(--kl-muted); border-radius: 14px; min-height: 32px; padding: 5px 8px; font-size: 13px; }
@@ -202,14 +294,58 @@
202
294
  .kl-job time { font-size: 11px; color: var(--kl-muted); }
203
295
  .kl-job > strong { display: block; overflow-wrap: anywhere; font-size: 13px; font-weight: 400; margin: 12px 0; }
204
296
  .kl-job > p { font-size: 12px; color: var(--kl-error); line-height: 1.7; }
205
- .kl-job button { display: flex; align-items: center; gap: 4px; color: var(--kl-link); font-size: 12px; border: 0; background: transparent; border-radius: 6px; padding: 4px; }
206
- .kl-job button:hover { background: var(--kl-hover); }
297
+ .kl-job button, .kl-job a { display: flex; align-items: center; gap: 4px; color: var(--kl-link); font-size: 12px; border: 0; background: transparent; border-radius: 6px; padding: 4px; text-decoration: none; }
298
+ .kl-job button:hover, .kl-job a:hover { background: var(--kl-hover); }
299
+ .kl-job .kl-job-actions { justify-content: flex-start; flex-wrap: wrap; gap: 8px; }
207
300
  .kl-job-status { font-size: 11px; border-radius: 6px; padding: 3px 7px; background: var(--kl-hover); color: var(--kl-secondary); }
208
301
  .kl-job-status.failed { color: var(--kl-error); }
209
302
  .kl-job-status.running { color: var(--kl-link); }
210
303
  .kl-info label { display: block; font-size: 12px; color: var(--kl-muted); margin-bottom: 8px; }
211
304
  .kl-info code { display: block; overflow-wrap: anywhere; background: var(--kl-sidebar); padding: 12px; border-radius: 8px; font-size: 12px; }
212
305
  .kl-info p { font-size: 13px; line-height: 1.8; color: var(--kl-secondary); margin: 16px 0; }
306
+ .kl-root-field, .kl-folder-path { display: flex; gap: 8px; align-items: center; }
307
+ .kl-root-field input, .kl-folder-path input { flex: 1; min-width: 0; border: .5px solid var(--kl-line); background: var(--kl-panel); border-radius: var(--kl-radius); padding: 10px 12px; font-size: 12px; }
308
+ .kl-root-field input[readonly] { background: var(--kl-sidebar); }
309
+ .kl-root-field button, .kl-folder-path button { flex-shrink: 0; }
310
+ .kl-root-confirm { margin-top: 14px; padding: 12px; border: .5px solid var(--kl-line); border-radius: 10px; background: var(--kl-sidebar); }
311
+ .kl-root-confirm p { margin: 0 0 12px; }
312
+ .kl-root-confirm > div, .kl-folder-actions { display: flex; justify-content: flex-end; gap: 8px; }
313
+ .kl-folder-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
314
+ .kl-folder-heading strong { font-size: 14px; font-weight: 500; }
315
+ .kl-folder-crumbs { display: flex; flex-wrap: wrap; gap: 4px; margin: 12px 0; }
316
+ .kl-folder-crumbs button { border: 0; background: transparent; padding: 3px 0; font-size: 12px; color: var(--kl-muted); overflow-wrap: anywhere; }
317
+ .kl-folder-crumbs span { padding: 0 6px; color: var(--kl-caption); }
318
+ .kl-folder-entries { height: 240px; overflow: auto; border: .5px solid var(--kl-line); border-radius: 10px; padding: 6px; }
319
+ .kl-folder-entries > button { width: 100%; display: flex; align-items: center; gap: 10px; text-align: left; border: 0; border-radius: 6px; padding: 10px; background: transparent; }
320
+ .kl-folder-entries > button:hover { background: var(--kl-hover); }
321
+ .kl-folder-entries > button span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
322
+ .kl-folder-entries > p { padding: 0 10px; }
323
+ .kl-folder-actions { margin-top: 16px; }
324
+ .kl-info .kl-settings-error { color: var(--kl-error); overflow-wrap: anywhere; }
325
+ .kl-network-settings { margin-top: 20px; padding-top: 16px; border-top: .5px solid var(--kl-line); }
326
+ .kl-network-settings h3 { font-size: 13px; font-weight: 500; margin: 0 0 12px; }
327
+ .kl-info .kl-proxy-option { display: flex; align-items: flex-start; gap: 8px; color: var(--kl-ink); font-size: 13px; line-height: 20px; cursor: pointer; }
328
+ .kl-proxy-option input { flex: none; width: 16px; height: 16px; margin: 2px 0; accent-color: var(--kl-ink); }
329
+ .kl-network-settings p { font-size: 12px; margin: 8px 0 12px; }
330
+ .kl-image-settings { margin-top: 20px; padding-top: 16px; border-top: .5px solid var(--kl-line); }
331
+ .kl-image-settings h3, .kl-page-images h3 { font-size: 13px; font-weight: 500; margin: 0 0 12px; }
332
+ .kl-image-settings p, .kl-image-record p { font-size: 12px; overflow-wrap: anywhere; }
333
+ .kl-info .kl-image-limit { display: flex; align-items: center; gap: 12px; margin: 14px 0; }
334
+ .kl-image-limit input { width: 68px; padding: 6px 8px; border: .5px solid var(--kl-line); border-radius: 8px; background: var(--kl-bg); }
335
+ .kl-image-policy { padding: 10px 12px; border-left: 2px solid var(--kl-link); background: var(--kl-sidebar); color: var(--kl-secondary); font-size: 12px; line-height: 1.8; margin: 0 0 16px; overflow-wrap: anywhere; }
336
+ .kl-page-images { margin: 24px 0; padding-block: 16px; border-block: .5px solid var(--kl-line); }
337
+ .kl-image-heading { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
338
+ .kl-image-heading h3 { margin: 0; }
339
+ .kl-image-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); gap: 16px; }
340
+ .kl-image-record { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 6px; font-size: 12px; overflow-wrap: anywhere; }
341
+ .kl-image-record > a:has(img) { width: 100%; }
342
+ .kl-image-record img { display: block; width: 100%; height: 140px; object-fit: contain; border: .5px solid var(--kl-line); border-radius: 8px; background: var(--kl-sidebar); }
343
+ .kl-image-record small { color: var(--kl-muted); }
344
+ .kl-image-record a { color: var(--kl-link); }
345
+ .kl-image-record p { margin: 0; color: var(--kl-secondary); }
346
+ .kl-delete-dialog li { overflow-wrap: anywhere; }
347
+ .kl-job > p { color: var(--kl-secondary); }
348
+ .kl-job > p[role="alert"] { color: var(--kl-error); }
213
349
  .kl-error, .kl-form-error { background: var(--kl-sidebar); color: var(--kl-error); font-size: 13px; line-height: 1.7; padding: 10px 12px; border-radius: 8px; overflow-wrap: anywhere; }
214
350
  .kl-error { display: flex; align-items: center; gap: 12px; margin: 0 var(--kl-gutter) 14px; }
215
351
  .kl-error button { margin-left: auto; border: 0; background: transparent; color: inherit; display: flex; padding: 4px; }
@@ -237,10 +373,14 @@
237
373
  .kl-toolbar { padding: 0 20px 12px; gap: 4px; }
238
374
  .kl-search kbd, .kl-job-button span, .kl-sort-label { display: none; }
239
375
  .kl-filterbar { padding: 0 20px 10px; }
376
+ .kl-trash-toolbar { padding: 0 20px 12px; flex-wrap: wrap; }
377
+ .kl-knowledge-toolbar { padding: 0 20px 12px; flex-wrap: wrap; }
240
378
  .kl-list { padding: 0 12px 16px; }
241
379
  .kl-empty { padding: 40px 12px; }
242
380
  .kl-detail article { padding: 20px; }
243
381
  .kl-detail-toolbar { padding: 8px 12px; }
382
+ .kl-detail-toolbar { flex-wrap: wrap; }
383
+ .kl-detail-toolbar > div { margin-left: auto; }
244
384
  .kl-error { margin: 0 20px 12px; }
245
385
  }
246
386
  /* Fixed dialogs use viewport width even when the host panel is narrow. */
@@ -248,6 +388,8 @@
248
388
  .kl-modal-backdrop { padding: 12px; }
249
389
  .kl-dialog { max-height: calc(100dvh - 24px); }
250
390
  .kl-dialog > header { padding: 20px 18px 16px; }
391
+ .kl-knowledge-form, .kl-knowledge-jobs { padding-inline: 18px; }
392
+ .kl-knowledge-fields { grid-template-columns: 1fr; gap: 0; }
251
393
  .kl-editor { padding: 0 18px; }
252
394
  .kl-editor footer { margin-left: -18px; margin-right: -18px; padding: 14px 18px; }
253
395
  .kl-editor footer > span { font-size: 10px; }
@@ -255,6 +397,7 @@
255
397
  .kl-tabs button { padding: 5px; font-size: 12px; }
256
398
  .kl-form-grid { gap: 12px; }
257
399
  .kl-jobs, .kl-info { padding: 0 18px 20px; }
400
+ .kl-delete-dialog { padding: 0 18px 20px; }
258
401
  }
259
402
  @media (prefers-reduced-motion: reduce) {
260
403
  .kl-app * { animation: none !important; transition: none !important; }