@cremini/skillpack 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/cli.js +1795 -712
- package/package.json +5 -2
- package/web/js/api-key-dialog.js +5 -0
- package/web/js/api.js +30 -0
- package/web/js/chat.js +290 -14
- package/web/js/main.js +7 -4
- package/web/styles.css +65 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cremini/skillpack",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "Pack AI Skills into Local Agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"build": "tsup",
|
|
29
29
|
"dev": "tsup --watch",
|
|
30
30
|
"check": "tsc --noEmit",
|
|
31
|
+
"test": "node --import tsx --test tests/*.test.ts",
|
|
31
32
|
"format": "prettier --write .",
|
|
32
33
|
"prepare": "npm run build",
|
|
33
34
|
"create": "tsup && node dist/cli.js create",
|
|
@@ -43,8 +44,8 @@
|
|
|
43
44
|
"author": "CreminiAI",
|
|
44
45
|
"license": "MIT",
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@sinclair/typebox": "^0.34.41",
|
|
47
47
|
"@mariozechner/pi-coding-agent": "^0.57.1",
|
|
48
|
+
"@sinclair/typebox": "^0.34.41",
|
|
48
49
|
"@slack/bolt": "^4.6.0",
|
|
49
50
|
"ajv": "^8.17.1",
|
|
50
51
|
"archiver": "^7.0.1",
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"inquirer": "^13.3.0",
|
|
55
56
|
"node-cron": "^4.2.1",
|
|
56
57
|
"node-telegram-bot-api": "^0.66.0",
|
|
58
|
+
"sqlite3": "^5.1.7",
|
|
57
59
|
"ws": "^8.19.0"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
@@ -66,6 +68,7 @@
|
|
|
66
68
|
"@types/ws": "^8.18.0",
|
|
67
69
|
"prettier": "^3.8.1",
|
|
68
70
|
"tsup": "^8.5.1",
|
|
71
|
+
"tsx": "^4.21.0",
|
|
69
72
|
"typescript": "^5.9.3"
|
|
70
73
|
}
|
|
71
74
|
}
|
package/web/js/api-key-dialog.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { state } from "./config.js";
|
|
7
7
|
import { saveConfigData, restartRuntime } from "./api.js";
|
|
8
|
+
import { refreshWebSocketConnectionPreference } from "./chat.js";
|
|
8
9
|
|
|
9
10
|
// --- DOM Elements ---
|
|
10
11
|
let dialog;
|
|
@@ -245,6 +246,7 @@ async function handleSave() {
|
|
|
245
246
|
populateForm();
|
|
246
247
|
state.restartRequired = !!res.requiresRestart;
|
|
247
248
|
updateApiKeyButton();
|
|
249
|
+
refreshWebSocketConnectionPreference();
|
|
248
250
|
|
|
249
251
|
if (res.requiresRestart) {
|
|
250
252
|
setStatus("Settings saved. Restart service to apply changes.", "warning");
|
|
@@ -301,6 +303,7 @@ async function handleOAuthLogout() {
|
|
|
301
303
|
updateOAuthUI(false);
|
|
302
304
|
state.config.oauthConnected = false;
|
|
303
305
|
updateApiKeyButton();
|
|
306
|
+
refreshWebSocketConnectionPreference();
|
|
304
307
|
setStatus("Logged out successfully", "success");
|
|
305
308
|
} catch (err) {
|
|
306
309
|
setStatus("Logout failed: " + err.message, "error");
|
|
@@ -314,6 +317,7 @@ async function checkOAuthStatus() {
|
|
|
314
317
|
updateOAuthUI(connected);
|
|
315
318
|
state.config.oauthConnected = connected;
|
|
316
319
|
updateApiKeyButton();
|
|
320
|
+
refreshWebSocketConnectionPreference();
|
|
317
321
|
} catch (err) {
|
|
318
322
|
console.error("Failed to check OAuth status:", err);
|
|
319
323
|
}
|
|
@@ -329,6 +333,7 @@ function pollOAuthStatus() {
|
|
|
329
333
|
state.config.oauthConnected = true;
|
|
330
334
|
state.restartRequired = true;
|
|
331
335
|
updateApiKeyButton();
|
|
336
|
+
refreshWebSocketConnectionPreference();
|
|
332
337
|
setStatus("Connected successfully!", "success");
|
|
333
338
|
updateRestartButton(true);
|
|
334
339
|
}
|
package/web/js/api.js
CHANGED
|
@@ -23,3 +23,33 @@ export async function restartRuntime() {
|
|
|
23
23
|
}
|
|
24
24
|
return payload;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
export async function listConversations() {
|
|
28
|
+
const res = await fetch(state.API_BASE + "/api/conversations");
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
throw new Error("Load Conversations Failed");
|
|
31
|
+
}
|
|
32
|
+
return await res.json();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function createConversation() {
|
|
36
|
+
const res = await fetch(state.API_BASE + "/api/conversations", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
throw new Error("Create Conversation Failed");
|
|
42
|
+
}
|
|
43
|
+
return await res.json();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function getConversationMessages(channelId, limit = 200) {
|
|
47
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
48
|
+
const res = await fetch(
|
|
49
|
+
state.API_BASE + `/api/conversations/${encodeURIComponent(channelId)}/messages?${params.toString()}`,
|
|
50
|
+
);
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error("Load Conversation Messages Failed");
|
|
53
|
+
}
|
|
54
|
+
return await res.json();
|
|
55
|
+
}
|
package/web/js/chat.js
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { state } from "./config.js";
|
|
2
|
+
import {
|
|
3
|
+
createConversation,
|
|
4
|
+
getConversationMessages,
|
|
5
|
+
listConversations,
|
|
6
|
+
} from "./api.js";
|
|
2
7
|
|
|
3
8
|
export const chatHistory = [];
|
|
9
|
+
const DEFAULT_WEB_CHANNEL_ID = "web";
|
|
4
10
|
let ws = null;
|
|
11
|
+
let wsConnectPromise = null;
|
|
5
12
|
let currentAssistantMsg = null;
|
|
13
|
+
let currentChannelId = DEFAULT_WEB_CHANNEL_ID;
|
|
14
|
+
const pendingSendFileCalls = new Map();
|
|
15
|
+
const anonymousSendFileCalls = [];
|
|
16
|
+
let reconnectTimer = null;
|
|
17
|
+
let shouldMaintainWs = false;
|
|
18
|
+
|
|
19
|
+
function hasConfiguredWebAuth() {
|
|
20
|
+
return Boolean(
|
|
21
|
+
state.config && (state.config.hasApiKey || state.config.oauthConnected)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
6
24
|
|
|
7
|
-
export function initChat() {
|
|
25
|
+
export async function initChat() {
|
|
8
26
|
// Send button
|
|
9
27
|
document.getElementById("send-btn").addEventListener("click", sendMessage);
|
|
10
28
|
|
|
@@ -39,6 +57,12 @@ export function initChat() {
|
|
|
39
57
|
}
|
|
40
58
|
});
|
|
41
59
|
}
|
|
60
|
+
|
|
61
|
+
await initializeConversation();
|
|
62
|
+
shouldMaintainWs = hasConfiguredWebAuth();
|
|
63
|
+
if (shouldMaintainWs) {
|
|
64
|
+
void ensureBackgroundWsConnection();
|
|
65
|
+
}
|
|
42
66
|
}
|
|
43
67
|
|
|
44
68
|
export function showWelcome(config) {
|
|
@@ -184,30 +208,90 @@ function renderEmbeddedMarkdownBlocks(html) {
|
|
|
184
208
|
return template.innerHTML;
|
|
185
209
|
}
|
|
186
210
|
|
|
187
|
-
|
|
211
|
+
function clearReconnectTimer() {
|
|
212
|
+
if (reconnectTimer !== null) {
|
|
213
|
+
window.clearTimeout(reconnectTimer);
|
|
214
|
+
reconnectTimer = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function scheduleReconnect() {
|
|
219
|
+
if (!shouldMaintainWs || !hasConfiguredWebAuth() || reconnectTimer !== null) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
reconnectTimer = window.setTimeout(() => {
|
|
224
|
+
reconnectTimer = null;
|
|
225
|
+
void ensureBackgroundWsConnection();
|
|
226
|
+
}, 1000);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function ensureBackgroundWsConnection() {
|
|
230
|
+
try {
|
|
231
|
+
await getOrCreateWs({ background: true });
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.warn("Background WebSocket connection failed:", err);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function refreshWebSocketConnectionPreference() {
|
|
238
|
+
shouldMaintainWs = hasConfiguredWebAuth();
|
|
239
|
+
|
|
240
|
+
if (shouldMaintainWs) {
|
|
241
|
+
void ensureBackgroundWsConnection();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
clearReconnectTimer();
|
|
246
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
247
|
+
ws.close();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function getOrCreateWs(options = {}) {
|
|
252
|
+
const { background = false } = options;
|
|
188
253
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
189
254
|
return ws;
|
|
190
255
|
}
|
|
191
256
|
|
|
192
|
-
|
|
257
|
+
if (wsConnectPromise) {
|
|
258
|
+
return wsConnectPromise;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
clearReconnectTimer();
|
|
262
|
+
|
|
263
|
+
wsConnectPromise = new Promise((resolve, reject) => {
|
|
193
264
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
194
265
|
const provider = state.config && state.config.provider ? state.config.provider : "openai";
|
|
266
|
+
const params = new URLSearchParams({
|
|
267
|
+
provider,
|
|
268
|
+
channelId: currentChannelId || DEFAULT_WEB_CHANNEL_ID,
|
|
269
|
+
});
|
|
270
|
+
const wsUrl = `${protocol}//${window.location.host}${state.API_BASE}/api/chat?${params.toString()}`;
|
|
195
271
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
ws = new WebSocket(wsUrl);
|
|
272
|
+
const socket = new WebSocket(wsUrl);
|
|
273
|
+
ws = socket;
|
|
199
274
|
|
|
200
|
-
|
|
201
|
-
|
|
275
|
+
socket.onopen = () => {
|
|
276
|
+
shouldMaintainWs = true;
|
|
277
|
+
wsConnectPromise = null;
|
|
278
|
+
resolve(socket);
|
|
279
|
+
};
|
|
280
|
+
socket.onerror = (err) => {
|
|
202
281
|
console.error(err);
|
|
282
|
+
wsConnectPromise = null;
|
|
203
283
|
reject(new Error("WebSocket connection failed"));
|
|
204
284
|
};
|
|
205
285
|
|
|
206
|
-
|
|
286
|
+
socket.onmessage = (event) => {
|
|
207
287
|
try {
|
|
208
288
|
const parsed = JSON.parse(event.data);
|
|
209
289
|
if (parsed.error) {
|
|
210
|
-
|
|
290
|
+
if (background && !currentAssistantMsg) {
|
|
291
|
+
console.warn("Background WebSocket message error:", parsed.error);
|
|
292
|
+
} else {
|
|
293
|
+
handleError(parsed.error);
|
|
294
|
+
}
|
|
211
295
|
} else if (parsed.done) {
|
|
212
296
|
handleDone();
|
|
213
297
|
} else if (parsed.type) {
|
|
@@ -218,11 +302,58 @@ async function getOrCreateWs() {
|
|
|
218
302
|
}
|
|
219
303
|
};
|
|
220
304
|
|
|
221
|
-
|
|
222
|
-
ws
|
|
305
|
+
socket.onclose = () => {
|
|
306
|
+
if (ws === socket) {
|
|
307
|
+
ws = null;
|
|
308
|
+
}
|
|
309
|
+
wsConnectPromise = null;
|
|
223
310
|
enableInput();
|
|
311
|
+
scheduleReconnect();
|
|
224
312
|
};
|
|
225
313
|
});
|
|
314
|
+
|
|
315
|
+
return wsConnectPromise;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function initializeConversation() {
|
|
319
|
+
const channelId = await ensureConversation();
|
|
320
|
+
currentChannelId = channelId;
|
|
321
|
+
await loadConversationHistory(channelId);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function ensureConversation() {
|
|
325
|
+
const conversations = await listConversations();
|
|
326
|
+
const existing = conversations.find(
|
|
327
|
+
(conversation) => conversation.channelId === DEFAULT_WEB_CHANNEL_ID,
|
|
328
|
+
);
|
|
329
|
+
if (existing) {
|
|
330
|
+
return existing.channelId;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const created = await createConversation();
|
|
334
|
+
return created.channelId || DEFAULT_WEB_CHANNEL_ID;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function loadConversationHistory(channelId) {
|
|
338
|
+
const messages = await getConversationMessages(channelId, 200);
|
|
339
|
+
const messagesEl = document.getElementById("messages");
|
|
340
|
+
const chatArea = document.getElementById("chat-area");
|
|
341
|
+
|
|
342
|
+
messagesEl.innerHTML = "";
|
|
343
|
+
chatHistory.length = 0;
|
|
344
|
+
|
|
345
|
+
for (const message of messages) {
|
|
346
|
+
appendMessage(message.role, message.text, message.toolCalls);
|
|
347
|
+
chatHistory.push({ role: message.role, content: message.text });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (messages.length > 0) {
|
|
351
|
+
chatArea.classList.remove("mode-welcome");
|
|
352
|
+
chatArea.classList.add("mode-chat");
|
|
353
|
+
} else {
|
|
354
|
+
chatArea.classList.remove("mode-chat");
|
|
355
|
+
chatArea.classList.add("mode-welcome");
|
|
356
|
+
}
|
|
226
357
|
}
|
|
227
358
|
|
|
228
359
|
function handleError(errorMsg) {
|
|
@@ -271,7 +402,28 @@ function hideLoadingIndicator() {
|
|
|
271
402
|
}
|
|
272
403
|
}
|
|
273
404
|
|
|
405
|
+
function ensureAssistantMessageForEvent(event) {
|
|
406
|
+
if (currentAssistantMsg) return;
|
|
407
|
+
|
|
408
|
+
if (
|
|
409
|
+
!["agent_start", "message_start", "text_delta", "thinking_delta", "tool_start"].includes(
|
|
410
|
+
event.type
|
|
411
|
+
)
|
|
412
|
+
) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const chatArea = document.getElementById("chat-area");
|
|
417
|
+
if (chatArea.classList.contains("mode-welcome")) {
|
|
418
|
+
chatArea.classList.remove("mode-welcome");
|
|
419
|
+
chatArea.classList.add("mode-chat");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
currentAssistantMsg = appendMessage("assistant", "");
|
|
423
|
+
}
|
|
424
|
+
|
|
274
425
|
function handleAgentEvent(event) {
|
|
426
|
+
ensureAssistantMessageForEvent(event);
|
|
275
427
|
if (!currentAssistantMsg) return;
|
|
276
428
|
|
|
277
429
|
if (
|
|
@@ -317,6 +469,13 @@ function handleAgentEvent(event) {
|
|
|
317
469
|
break;
|
|
318
470
|
|
|
319
471
|
case "tool_start":
|
|
472
|
+
if (event.toolName === "send_file") {
|
|
473
|
+
queueSendFileToolCall(event.toolCallId, event.toolInput);
|
|
474
|
+
scrollToBottom();
|
|
475
|
+
showLoadingIndicator();
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
|
|
320
479
|
const toolCard = document.createElement("div");
|
|
321
480
|
toolCard.className = "tool-card running collapsed";
|
|
322
481
|
const safeInput =
|
|
@@ -360,13 +519,27 @@ function handleAgentEvent(event) {
|
|
|
360
519
|
}
|
|
361
520
|
|
|
362
521
|
toolCard.dataset.toolName = event.toolName;
|
|
522
|
+
toolCard.dataset.toolCallId = event.toolCallId || "";
|
|
363
523
|
scrollToBottom();
|
|
364
524
|
showLoadingIndicator();
|
|
365
525
|
break;
|
|
366
526
|
|
|
367
527
|
case "tool_end":
|
|
528
|
+
if (event.toolName === "send_file") {
|
|
529
|
+
const file = dequeueSendFileToolCall(event.toolCallId);
|
|
530
|
+
if (!event.isError && file) {
|
|
531
|
+
appendFileCard(currentAssistantMsg, file.filePath, file.caption);
|
|
532
|
+
}
|
|
533
|
+
scrollToBottom();
|
|
534
|
+
showLoadingIndicator();
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
|
|
368
538
|
const cards = Array.from(currentAssistantMsg.querySelectorAll(".tool-card.running"));
|
|
369
|
-
const card = cards.reverse().find((c) =>
|
|
539
|
+
const card = cards.reverse().find((c) =>
|
|
540
|
+
c.dataset.toolName === event.toolName &&
|
|
541
|
+
(event.toolCallId ? c.dataset.toolCallId === event.toolCallId : true)
|
|
542
|
+
);
|
|
370
543
|
if (card) {
|
|
371
544
|
card.classList.remove("running");
|
|
372
545
|
card.classList.add(event.isError ? "error" : "success");
|
|
@@ -405,6 +578,100 @@ function handleAgentEvent(event) {
|
|
|
405
578
|
}
|
|
406
579
|
}
|
|
407
580
|
|
|
581
|
+
function queueSendFileToolCall(toolCallId, toolInput) {
|
|
582
|
+
const file = extractSendFileToolInput(toolInput);
|
|
583
|
+
if (!file) return;
|
|
584
|
+
|
|
585
|
+
if (toolCallId) {
|
|
586
|
+
pendingSendFileCalls.set(toolCallId, file);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
anonymousSendFileCalls.push(file);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function dequeueSendFileToolCall(toolCallId) {
|
|
594
|
+
if (toolCallId) {
|
|
595
|
+
const file = pendingSendFileCalls.get(toolCallId) || null;
|
|
596
|
+
pendingSendFileCalls.delete(toolCallId);
|
|
597
|
+
return file;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return anonymousSendFileCalls.shift() || null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function extractSendFileToolInput(toolInput) {
|
|
604
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const filePath =
|
|
609
|
+
typeof toolInput.filePath === "string" ? toolInput.filePath.trim() : "";
|
|
610
|
+
const caption =
|
|
611
|
+
typeof toolInput.caption === "string" ? toolInput.caption.trim() : "";
|
|
612
|
+
|
|
613
|
+
if (!filePath) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
filePath,
|
|
619
|
+
caption,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function getVisibleSendFileToolCalls(toolCalls) {
|
|
624
|
+
if (!Array.isArray(toolCalls)) {
|
|
625
|
+
return [];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return toolCalls.filter((toolCall) =>
|
|
629
|
+
toolCall &&
|
|
630
|
+
toolCall.name === "send_file" &&
|
|
631
|
+
!toolCall.isError &&
|
|
632
|
+
toolCall.arguments &&
|
|
633
|
+
typeof toolCall.arguments.filePath === "string" &&
|
|
634
|
+
toolCall.arguments.filePath
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function appendFileCard(container, filePath, caption) {
|
|
639
|
+
const fileName = basename(filePath);
|
|
640
|
+
const title = caption || fileName;
|
|
641
|
+
const card = document.createElement("a");
|
|
642
|
+
card.className = "file-card";
|
|
643
|
+
card.href = buildFileDownloadUrl(filePath);
|
|
644
|
+
card.title = filePath;
|
|
645
|
+
card.setAttribute("download", fileName);
|
|
646
|
+
card.setAttribute("target", "_blank");
|
|
647
|
+
card.setAttribute("rel", "noopener noreferrer");
|
|
648
|
+
card.innerHTML = `
|
|
649
|
+
<div class="file-card-icon">FILE</div>
|
|
650
|
+
<div class="file-card-copy">
|
|
651
|
+
<div class="file-card-title">${escapeHtml(title)}</div>
|
|
652
|
+
<div class="file-card-meta">${escapeHtml(fileName)}</div>
|
|
653
|
+
</div>
|
|
654
|
+
<div class="file-card-action">Download</div>
|
|
655
|
+
`;
|
|
656
|
+
|
|
657
|
+
const indicator = container.querySelector(".loading-indicator");
|
|
658
|
+
if (indicator) {
|
|
659
|
+
container.insertBefore(card, indicator);
|
|
660
|
+
} else {
|
|
661
|
+
container.appendChild(card);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function basename(filePath) {
|
|
666
|
+
const normalized = String(filePath).replace(/\\/g, "/");
|
|
667
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
668
|
+
return parts[parts.length - 1] || String(filePath);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function buildFileDownloadUrl(filePath) {
|
|
672
|
+
return `${state.API_BASE}/api/files?path=${encodeURIComponent(filePath)}`;
|
|
673
|
+
}
|
|
674
|
+
|
|
408
675
|
function getOrCreateThinkingBlock() {
|
|
409
676
|
const children = Array.from(currentAssistantMsg.children).filter(
|
|
410
677
|
(c) => !c.classList.contains("loading-indicator")
|
|
@@ -465,10 +732,12 @@ function getOrCreateTextBlock() {
|
|
|
465
732
|
function enableInput() {
|
|
466
733
|
const sendBtn = document.getElementById("send-btn");
|
|
467
734
|
if (sendBtn) sendBtn.disabled = false;
|
|
735
|
+
pendingSendFileCalls.clear();
|
|
736
|
+
anonymousSendFileCalls.length = 0;
|
|
468
737
|
currentAssistantMsg = null;
|
|
469
738
|
}
|
|
470
739
|
|
|
471
|
-
function appendMessage(role, text) {
|
|
740
|
+
function appendMessage(role, text, toolCalls = []) {
|
|
472
741
|
const messages = document.getElementById("messages");
|
|
473
742
|
const div = document.createElement("div");
|
|
474
743
|
div.className = "message " + role;
|
|
@@ -483,6 +752,13 @@ function appendMessage(role, text) {
|
|
|
483
752
|
div.appendChild(tb);
|
|
484
753
|
}
|
|
485
754
|
|
|
755
|
+
if (role === "assistant") {
|
|
756
|
+
const sendFileCalls = getVisibleSendFileToolCalls(toolCalls);
|
|
757
|
+
sendFileCalls.forEach((toolCall) => {
|
|
758
|
+
appendFileCard(div, toolCall.arguments.filePath, toolCall.arguments.caption);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
486
762
|
messages.appendChild(div);
|
|
487
763
|
scrollToBottom();
|
|
488
764
|
return div;
|
package/web/js/main.js
CHANGED
|
@@ -39,13 +39,16 @@ async function init() {
|
|
|
39
39
|
} catch (err) {
|
|
40
40
|
console.error("Initialization Failed:", err);
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
// Initialize all dialog modules
|
|
42
|
+
|
|
44
43
|
initApiKeyDialog();
|
|
45
44
|
initChatAppsDialog();
|
|
46
|
-
initChat();
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
try {
|
|
47
|
+
await initChat();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error("Chat initialization failed:", err);
|
|
50
|
+
}
|
|
51
|
+
|
|
49
52
|
updateApiKeyButton();
|
|
50
53
|
updateChatAppsButton();
|
|
51
54
|
}
|
package/web/styles.css
CHANGED
|
@@ -646,6 +646,71 @@ body {
|
|
|
646
646
|
color: var(--text-primary);
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
+
.file-card {
|
|
650
|
+
display: flex;
|
|
651
|
+
align-items: center;
|
|
652
|
+
gap: 14px;
|
|
653
|
+
padding: 14px 16px;
|
|
654
|
+
margin: 0;
|
|
655
|
+
border: 1px solid var(--border-color);
|
|
656
|
+
border-radius: var(--radius);
|
|
657
|
+
background: linear-gradient(135deg, #f9fafb 0%, #f1f4f7 100%);
|
|
658
|
+
color: inherit;
|
|
659
|
+
text-decoration: none;
|
|
660
|
+
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.file-card:hover {
|
|
664
|
+
transform: translateY(-1px);
|
|
665
|
+
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
|
|
666
|
+
border-color: rgba(0, 122, 255, 0.24);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.file-card-icon {
|
|
670
|
+
flex: 0 0 auto;
|
|
671
|
+
min-width: 46px;
|
|
672
|
+
height: 46px;
|
|
673
|
+
padding: 0 10px;
|
|
674
|
+
border-radius: 12px;
|
|
675
|
+
background: #0f172a;
|
|
676
|
+
color: #ffffff;
|
|
677
|
+
display: inline-flex;
|
|
678
|
+
align-items: center;
|
|
679
|
+
justify-content: center;
|
|
680
|
+
font-size: 11px;
|
|
681
|
+
font-weight: 700;
|
|
682
|
+
letter-spacing: 0.08em;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.file-card-copy {
|
|
686
|
+
min-width: 0;
|
|
687
|
+
flex: 1 1 auto;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.file-card-title {
|
|
691
|
+
font-weight: 600;
|
|
692
|
+
color: var(--text-primary);
|
|
693
|
+
line-height: 1.35;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.file-card-meta {
|
|
697
|
+
margin-top: 4px;
|
|
698
|
+
color: var(--text-secondary);
|
|
699
|
+
font-size: 12px;
|
|
700
|
+
word-break: break-all;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.file-card-action {
|
|
704
|
+
flex: 0 0 auto;
|
|
705
|
+
padding: 8px 12px;
|
|
706
|
+
border-radius: 999px;
|
|
707
|
+
background: #ffffff;
|
|
708
|
+
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
709
|
+
color: var(--accent);
|
|
710
|
+
font-size: 12px;
|
|
711
|
+
font-weight: 600;
|
|
712
|
+
}
|
|
713
|
+
|
|
649
714
|
/* Markdown Specifics */
|
|
650
715
|
.markdown-body pre {
|
|
651
716
|
background: #f8f9fa;
|