openclacky 0.9.30 → 0.9.32
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/lib/clacky/agent/llm_caller.rb +5 -5
- data/lib/clacky/agent/memory_updater.rb +1 -1
- data/lib/clacky/agent/session_serializer.rb +2 -1
- data/lib/clacky/agent/skill_auto_creator.rb +119 -0
- data/lib/clacky/agent/skill_evolution.rb +46 -0
- data/lib/clacky/agent/skill_manager.rb +8 -0
- data/lib/clacky/agent/skill_reflector.rb +97 -0
- data/lib/clacky/agent.rb +38 -12
- data/lib/clacky/agent_config.rb +10 -1
- data/lib/clacky/brand_config.rb +23 -0
- data/lib/clacky/cli.rb +1 -1
- data/lib/clacky/default_skills/onboard/SKILL.md +15 -7
- data/lib/clacky/default_skills/personal-website/publish.rb +1 -1
- data/lib/clacky/default_skills/skill-creator/SKILL.md +46 -0
- data/lib/clacky/json_ui_controller.rb +0 -4
- data/lib/clacky/message_history.rb +0 -12
- data/lib/clacky/plain_ui_controller.rb +19 -1
- data/lib/clacky/platform_http_client.rb +2 -4
- data/lib/clacky/providers.rb +12 -1
- data/lib/clacky/server/channel/channel_ui_controller.rb +0 -2
- data/lib/clacky/server/http_server.rb +13 -1
- data/lib/clacky/server/web_ui_controller.rb +55 -29
- data/lib/clacky/tools/shell.rb +91 -170
- data/lib/clacky/ui2/ui_controller.rb +100 -93
- data/lib/clacky/ui_interface.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +5 -2
- data/lib/clacky/utils/limit_stack.rb +81 -13
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +247 -51
- data/lib/clacky/web/app.js +11 -3
- data/lib/clacky/web/brand.js +21 -3
- data/lib/clacky/web/creator.js +13 -2
- data/lib/clacky/web/i18n.js +41 -15
- data/lib/clacky/web/index.html +38 -20
- data/lib/clacky/web/sessions.js +256 -57
- data/lib/clacky/web/settings.js +32 -0
- data/lib/clacky/web/skills.js +61 -1
- metadata +4 -1
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -572,7 +572,15 @@ const Sessions = (() => {
|
|
|
572
572
|
messages.scrollTop = messages.scrollHeight - scrollBefore;
|
|
573
573
|
} else {
|
|
574
574
|
// Initial load or append: scroll to bottom (user just opened session or sent message)
|
|
575
|
-
|
|
575
|
+
// If a progress indicator is already visible (attached instantly on session switch),
|
|
576
|
+
// insert history above it so the progress element stays at the bottom.
|
|
577
|
+
const pState = Sessions._sessionProgress[id];
|
|
578
|
+
const existingProgressEl = pState && pState.el;
|
|
579
|
+
if (existingProgressEl && existingProgressEl.parentNode === messages) {
|
|
580
|
+
messages.insertBefore(frag, existingProgressEl);
|
|
581
|
+
} else {
|
|
582
|
+
messages.appendChild(frag);
|
|
583
|
+
}
|
|
576
584
|
messages.scrollTop = messages.scrollHeight;
|
|
577
585
|
// Flush any tool_stdout lines that arrived via WS before this history
|
|
578
586
|
// fetch completed (race condition on session switch).
|
|
@@ -595,8 +603,10 @@ const Sessions = (() => {
|
|
|
595
603
|
const session = _sessions.find(s => s.id === id);
|
|
596
604
|
if (session) {
|
|
597
605
|
if (session.status === "running") {
|
|
598
|
-
//
|
|
599
|
-
|
|
606
|
+
// Progress UI is already attached (done eagerly in Router._apply).
|
|
607
|
+
// The backend's replay_live_state event will arrive shortly and call
|
|
608
|
+
// showProgress() with the authoritative started_at, which is the
|
|
609
|
+
// single source of truth for first-visit sessions (no cached state).
|
|
600
610
|
} else if (session.status === "error" && session.error) {
|
|
601
611
|
// Show the stored error message at the end of history
|
|
602
612
|
Sessions.appendMsg("error", session.error);
|
|
@@ -768,6 +778,8 @@ const Sessions = (() => {
|
|
|
768
778
|
remove(id) {
|
|
769
779
|
const idx = _sessions.findIndex(s => s.id === id);
|
|
770
780
|
if (idx !== -1) _sessions.splice(idx, 1);
|
|
781
|
+
// Clean up per-session progress state (timer + DOM + logical state)
|
|
782
|
+
Sessions._deleteProgressState(id);
|
|
771
783
|
},
|
|
772
784
|
|
|
773
785
|
/** Load the next page of older sessions (unified time cursor). */
|
|
@@ -964,6 +976,9 @@ const Sessions = (() => {
|
|
|
964
976
|
* Called by Router before switching away from a session view. */
|
|
965
977
|
_cacheActiveAndDeselect() {
|
|
966
978
|
_cacheActiveMessages();
|
|
979
|
+
// Detach progress UI (DOM + timer) but preserve the logical state
|
|
980
|
+
// so it can be restored when the user switches back to this session.
|
|
981
|
+
if (_activeId) Sessions._detachProgressUI(_activeId);
|
|
967
982
|
_activeId = null;
|
|
968
983
|
WS.setSubscribedSession(null);
|
|
969
984
|
Sessions.renderList();
|
|
@@ -1019,7 +1034,16 @@ const Sessions = (() => {
|
|
|
1019
1034
|
|
|
1020
1035
|
// Scroll active session into view so the sidebar always shows the current session.
|
|
1021
1036
|
const activeEl = list.querySelector(".session-item.active");
|
|
1022
|
-
if (activeEl)
|
|
1037
|
+
if (activeEl) {
|
|
1038
|
+
// If the active session is the very first item, scroll to top of the sidebar
|
|
1039
|
+
// container so sticky headers / expanded panels don't obscure it.
|
|
1040
|
+
if (activeEl === list.firstElementChild) {
|
|
1041
|
+
const sidebarList = document.getElementById("sidebar-list");
|
|
1042
|
+
if (sidebarList) sidebarList.scrollTop = 0;
|
|
1043
|
+
} else {
|
|
1044
|
+
activeEl.scrollIntoView({ block: "nearest" });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1023
1047
|
},
|
|
1024
1048
|
|
|
1025
1049
|
/** Begin inline rename: replace session-name content with an <input>. */
|
|
@@ -1314,79 +1338,254 @@ const Sessions = (() => {
|
|
|
1314
1338
|
_scrollToBottomIfNeeded(messages);
|
|
1315
1339
|
},
|
|
1316
1340
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
showProgress(text, progress_type = "thinking", metadata = {}) {
|
|
1323
|
-
console.log("[DEBUG] showProgress called:", { text, progress_type, metadata });
|
|
1324
|
-
Sessions.clearProgress();
|
|
1325
|
-
|
|
1326
|
-
Sessions._progressType = progress_type;
|
|
1327
|
-
Sessions._progressStartTime = Date.now();
|
|
1328
|
-
|
|
1341
|
+
// Display a request_user_feedback UI card with optional clickable option buttons.
|
|
1342
|
+
// Called when the agent needs user input to continue.
|
|
1343
|
+
showFeedbackRequest(question, context, options) {
|
|
1344
|
+
Sessions.collapseToolGroup();
|
|
1329
1345
|
const messages = $("messages");
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1346
|
+
const hasOptions = options && Array.isArray(options) && options.length > 0;
|
|
1347
|
+
|
|
1348
|
+
// Normalize bullet symbols to markdown list format so marked renders them as <ul>
|
|
1349
|
+
const normalizeBullets = (text) => text ? text.replace(/^[•·‣▸▪\-–]\s*/gm, '- ') : text;
|
|
1350
|
+
|
|
1351
|
+
// No options → plain assistant bubble (card UI adds no value without choices)
|
|
1352
|
+
if (!hasOptions) {
|
|
1353
|
+
const parts = [context && context.trim(), question].filter(Boolean);
|
|
1354
|
+
const text = parts.map(normalizeBullets).join("\n\n");
|
|
1355
|
+
Sessions.appendMsg("assistant", marked.parse(text));
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Has options → render interactive card
|
|
1360
|
+
const card = document.createElement("div");
|
|
1361
|
+
card.className = "feedback-card";
|
|
1362
|
+
|
|
1363
|
+
let cardHtml = "";
|
|
1364
|
+
if (context && context.trim()) {
|
|
1365
|
+
cardHtml += `<div class="feedback-context">${escapeHtml(context)}</div>`;
|
|
1366
|
+
}
|
|
1367
|
+
cardHtml += `<div class="feedback-question">${escapeHtml(question)}</div>`;
|
|
1368
|
+
cardHtml += `<div class="feedback-options">`;
|
|
1369
|
+
options.forEach((opt, idx) => {
|
|
1370
|
+
cardHtml += `<button class="feedback-option-btn" data-option-index="${idx}">${escapeHtml(opt)}</button>`;
|
|
1371
|
+
});
|
|
1372
|
+
cardHtml += `</div>`;
|
|
1373
|
+
cardHtml += `<div class="feedback-hint">${I18n.t("chat.feedback_hint")}</div>`;
|
|
1374
|
+
|
|
1375
|
+
card.innerHTML = cardHtml;
|
|
1376
|
+
|
|
1377
|
+
// Click → disable card + submit immediately via sendMessage()
|
|
1378
|
+
card.querySelectorAll(".feedback-option-btn").forEach(btn => {
|
|
1379
|
+
btn.onclick = () => {
|
|
1380
|
+
card.querySelectorAll(".feedback-option-btn").forEach(b => b.disabled = true);
|
|
1381
|
+
card.classList.add("feedback-card--submitted");
|
|
1382
|
+
const input = $("user-input");
|
|
1383
|
+
if (input) input.value = btn.textContent.trim();
|
|
1384
|
+
sendMessage();
|
|
1385
|
+
};
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
messages.appendChild(card);
|
|
1389
|
+
_scrollToBottomIfNeeded(messages);
|
|
1390
|
+
},
|
|
1391
|
+
|
|
1392
|
+
// ── Per-session progress state ──────────────────────────────────────
|
|
1393
|
+
//
|
|
1394
|
+
// Each session maintains its own progress state so switching sessions
|
|
1395
|
+
// and switching back does NOT reset the elapsed timer.
|
|
1396
|
+
//
|
|
1397
|
+
// State map: { [sessionId]: { el, interval, startTime, type, displayText } }
|
|
1398
|
+
// el — DOM element (.progress-msg) currently in #messages (or null if detached)
|
|
1399
|
+
// interval — setInterval id for the ticking counter (or null if detached)
|
|
1400
|
+
// startTime — Date.now()-compatible ms timestamp when progress began
|
|
1401
|
+
// type — "thinking" | "retrying" | "idle_compress" | …
|
|
1402
|
+
// displayText — the label shown before the "(Ns)" suffix
|
|
1403
|
+
|
|
1404
|
+
_sessionProgress: {},
|
|
1405
|
+
|
|
1406
|
+
_getProgressState(id) {
|
|
1407
|
+
if (!id) return null;
|
|
1408
|
+
if (!Sessions._sessionProgress[id]) {
|
|
1409
|
+
Sessions._sessionProgress[id] = { el: null, interval: null, startTime: null, type: null, displayText: null };
|
|
1410
|
+
}
|
|
1411
|
+
return Sessions._sessionProgress[id];
|
|
1412
|
+
},
|
|
1413
|
+
|
|
1414
|
+
// Build the display label for a given progress type (pure — no side effects).
|
|
1415
|
+
_buildDisplayText(text, progress_type, metadata) {
|
|
1335
1416
|
if (progress_type === "thinking") {
|
|
1336
|
-
|
|
1337
|
-
console.log("[DEBUG] thinking verb:", displayText);
|
|
1417
|
+
return text || getRandomThinkingVerb();
|
|
1338
1418
|
} else if (progress_type === "retrying") {
|
|
1339
|
-
const { attempt, total } = metadata;
|
|
1340
|
-
// Show error reason + retry count
|
|
1419
|
+
const { attempt, total } = metadata || {};
|
|
1341
1420
|
if (text && attempt && total) {
|
|
1342
|
-
|
|
1421
|
+
return `${I18n.t("chat.retrying")}: ${text} (${attempt}/${total})`;
|
|
1343
1422
|
} else if (attempt && total) {
|
|
1344
|
-
|
|
1345
|
-
} else {
|
|
1346
|
-
displayText = text || I18n.t("chat.retrying");
|
|
1423
|
+
return `${I18n.t("chat.retrying")} (${attempt}/${total})`;
|
|
1347
1424
|
}
|
|
1425
|
+
return text || I18n.t("chat.retrying");
|
|
1348
1426
|
} else if (progress_type === "idle_compress") {
|
|
1349
|
-
|
|
1350
|
-
} else {
|
|
1351
|
-
displayText = text || I18n.t("chat.thinking");
|
|
1427
|
+
return text || "Compressing...";
|
|
1352
1428
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1429
|
+
return text || I18n.t("chat.thinking");
|
|
1430
|
+
},
|
|
1431
|
+
|
|
1432
|
+
// Attach the progress UI (DOM element + setInterval) for a given session.
|
|
1433
|
+
// Requires the session's progress state to already have startTime + displayText set.
|
|
1434
|
+
_attachProgressUI(id) {
|
|
1435
|
+
const state = Sessions._getProgressState(id);
|
|
1436
|
+
if (!state || !state.startTime) return;
|
|
1437
|
+
|
|
1438
|
+
// Only attach if this session is currently visible
|
|
1439
|
+
if (id !== _activeId) return;
|
|
1440
|
+
|
|
1441
|
+
const messages = $("messages");
|
|
1442
|
+
if (!messages) return;
|
|
1443
|
+
|
|
1444
|
+
// Clean up any previous DOM/timer for this session (idempotent)
|
|
1445
|
+
Sessions._detachProgressUI(id);
|
|
1446
|
+
|
|
1447
|
+
const el = document.createElement("div");
|
|
1448
|
+
el.className = "progress-msg";
|
|
1449
|
+
const displayText = state.displayText;
|
|
1450
|
+
// Show elapsed time immediately (not just after first setInterval tick)
|
|
1451
|
+
const initialElapsed = Math.floor((Date.now() - state.startTime) / 1000);
|
|
1452
|
+
el.textContent = initialElapsed > 0
|
|
1453
|
+
? `⟳ ${displayText}… (${initialElapsed}s)`
|
|
1454
|
+
: `⟳ ${displayText}`;
|
|
1356
1455
|
messages.appendChild(el);
|
|
1357
|
-
|
|
1456
|
+
state.el = el;
|
|
1358
1457
|
_scrollToBottomIfNeeded(messages);
|
|
1359
|
-
|
|
1458
|
+
|
|
1360
1459
|
// Start elapsed time counter (update every second)
|
|
1361
|
-
|
|
1362
|
-
const elapsed = Math.floor((Date.now() -
|
|
1363
|
-
if (
|
|
1364
|
-
|
|
1460
|
+
state.interval = setInterval(() => {
|
|
1461
|
+
const elapsed = Math.floor((Date.now() - state.startTime) / 1000);
|
|
1462
|
+
if (state.el) {
|
|
1463
|
+
state.el.textContent = `⟳ ${displayText}… (${elapsed}s)`;
|
|
1365
1464
|
}
|
|
1366
1465
|
}, 1000);
|
|
1367
1466
|
},
|
|
1368
1467
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1468
|
+
// Detach only the DOM element and timer for a session, preserving logical state
|
|
1469
|
+
// (startTime, type, displayText). Called when switching away from a session.
|
|
1470
|
+
_detachProgressUI(id) {
|
|
1471
|
+
const state = Sessions._sessionProgress[id];
|
|
1472
|
+
if (!state) return;
|
|
1473
|
+
if (state.interval) {
|
|
1474
|
+
clearInterval(state.interval);
|
|
1475
|
+
state.interval = null;
|
|
1375
1476
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
Sessions._progressEl.remove();
|
|
1380
|
-
Sessions._progressEl = null;
|
|
1477
|
+
if (state.el) {
|
|
1478
|
+
state.el.remove();
|
|
1479
|
+
state.el = null;
|
|
1381
1480
|
}
|
|
1382
|
-
|
|
1481
|
+
},
|
|
1482
|
+
|
|
1483
|
+
showProgress(text, progress_type = "thinking", metadata = {}, startedAt = null) {
|
|
1484
|
+
const sid = _activeId;
|
|
1485
|
+
if (!sid) return;
|
|
1486
|
+
|
|
1487
|
+
const newStartTime = startedAt || Date.now();
|
|
1488
|
+
const newDisplayText = Sessions._buildDisplayText(text, progress_type, metadata);
|
|
1489
|
+
|
|
1490
|
+
// If this session already has a visible progress indicator (DOM element
|
|
1491
|
+
// attached), update it in-place instead of tear-down/rebuild. This avoids
|
|
1492
|
+
// the jarring flicker when replay_live_state arrives shortly after the
|
|
1493
|
+
// eager-attach on session switch.
|
|
1494
|
+
const existing = Sessions._sessionProgress[sid];
|
|
1495
|
+
if (existing && existing.el) {
|
|
1496
|
+
// If the start time is the same (same progress phase, e.g. dedup replay),
|
|
1497
|
+
// keep everything as-is — not even the display text changes.
|
|
1498
|
+
if (existing.startTime === newStartTime) {
|
|
1499
|
+
existing.type = progress_type;
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
// Different start time → new progress phase. Update state in-place and
|
|
1503
|
+
// restart the interval, but reuse the existing DOM element so the user
|
|
1504
|
+
// never sees the indicator disappear/reappear.
|
|
1505
|
+
existing.type = progress_type;
|
|
1506
|
+
existing.startTime = newStartTime;
|
|
1507
|
+
existing.displayText = newDisplayText;
|
|
1508
|
+
// Immediately refresh the text + elapsed counter
|
|
1509
|
+
const elapsed = Math.floor((Date.now() - newStartTime) / 1000);
|
|
1510
|
+
existing.el.textContent = elapsed > 0
|
|
1511
|
+
? `⟳ ${newDisplayText}… (${elapsed}s)`
|
|
1512
|
+
: `⟳ ${newDisplayText}`;
|
|
1513
|
+
// Restart interval with new startTime
|
|
1514
|
+
if (existing.interval) clearInterval(existing.interval);
|
|
1515
|
+
existing.interval = setInterval(() => {
|
|
1516
|
+
const e = Math.floor((Date.now() - existing.startTime) / 1000);
|
|
1517
|
+
if (existing.el) {
|
|
1518
|
+
existing.el.textContent = `⟳ ${existing.displayText}… (${e}s)`;
|
|
1519
|
+
}
|
|
1520
|
+
}, 1000);
|
|
1521
|
+
_scrollToBottomIfNeeded($("messages"));
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// No existing visible progress — create from scratch.
|
|
1526
|
+
// Clear any stale logical state first.
|
|
1527
|
+
Sessions.clearProgress(sid);
|
|
1528
|
+
|
|
1529
|
+
const state = Sessions._getProgressState(sid);
|
|
1530
|
+
state.type = progress_type;
|
|
1531
|
+
state.startTime = newStartTime;
|
|
1532
|
+
state.displayText = newDisplayText;
|
|
1533
|
+
|
|
1534
|
+
// Attach DOM + timer
|
|
1535
|
+
Sessions._attachProgressUI(sid);
|
|
1536
|
+
},
|
|
1537
|
+
|
|
1538
|
+
clearProgress(sessionIdOrMessage = null, finalMessage = null) {
|
|
1539
|
+
// Backward-compatible overload resolution:
|
|
1540
|
+
// clearProgress() — clear active session
|
|
1541
|
+
// clearProgress("some message") — clear active session + final message
|
|
1542
|
+
// clearProgress(sessionId) — clear specific session (id looks like UUID)
|
|
1543
|
+
// clearProgress(sessionId, "message") — clear specific session + final message
|
|
1544
|
+
let sid;
|
|
1545
|
+
if (sessionIdOrMessage && typeof sessionIdOrMessage === "string") {
|
|
1546
|
+
// Heuristic: session IDs are UUIDs (contain hyphens or are 32+ hex chars).
|
|
1547
|
+
// Anything else is treated as a finalMessage for the active session.
|
|
1548
|
+
if (/^[0-9a-f-]{8,}$/i.test(sessionIdOrMessage)) {
|
|
1549
|
+
sid = sessionIdOrMessage;
|
|
1550
|
+
} else {
|
|
1551
|
+
finalMessage = sessionIdOrMessage;
|
|
1552
|
+
sid = _activeId;
|
|
1553
|
+
}
|
|
1554
|
+
} else {
|
|
1555
|
+
sid = _activeId;
|
|
1556
|
+
}
|
|
1557
|
+
if (!sid) return;
|
|
1558
|
+
|
|
1559
|
+
const state = Sessions._sessionProgress[sid];
|
|
1560
|
+
if (!state) return;
|
|
1561
|
+
|
|
1562
|
+
// Detach DOM + timer
|
|
1563
|
+
Sessions._detachProgressUI(sid);
|
|
1564
|
+
|
|
1383
1565
|
// Show final message if provided (for idle_compress, etc.)
|
|
1384
|
-
if (finalMessage &&
|
|
1566
|
+
if (finalMessage && state.type && state.type !== "thinking") {
|
|
1385
1567
|
Sessions.appendInfo(`· ${finalMessage}`);
|
|
1386
1568
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1569
|
+
|
|
1570
|
+
// Clear logical state
|
|
1571
|
+
state.startTime = null;
|
|
1572
|
+
state.type = null;
|
|
1573
|
+
state.displayText = null;
|
|
1574
|
+
},
|
|
1575
|
+
|
|
1576
|
+
// Delete all progress state for a session (used when session is removed).
|
|
1577
|
+
_deleteProgressState(id) {
|
|
1578
|
+
Sessions._detachProgressUI(id);
|
|
1579
|
+
delete Sessions._sessionProgress[id];
|
|
1580
|
+
},
|
|
1581
|
+
|
|
1582
|
+
// Clear progress for ALL sessions (used on WS disconnect).
|
|
1583
|
+
clearAllProgress() {
|
|
1584
|
+
for (const id of Object.keys(Sessions._sessionProgress)) {
|
|
1585
|
+
Sessions._detachProgressUI(id);
|
|
1586
|
+
}
|
|
1587
|
+
// Wipe the entire map — all state is stale after disconnect
|
|
1588
|
+
Sessions._sessionProgress = {};
|
|
1390
1589
|
},
|
|
1391
1590
|
|
|
1392
1591
|
// ── Create ─────────────────────────────────────────────────────────────
|
data/lib/clacky/web/settings.js
CHANGED
|
@@ -750,6 +750,38 @@ const Settings = (() => {
|
|
|
750
750
|
document.getElementById("settings-license-key").focus();
|
|
751
751
|
});
|
|
752
752
|
|
|
753
|
+
document.getElementById("btn-unbind-license").addEventListener("click", async () => {
|
|
754
|
+
const confirmed = await Modal.confirm(I18n.t("settings.brand.confirmUnbind"));
|
|
755
|
+
if (!confirmed) return;
|
|
756
|
+
|
|
757
|
+
try {
|
|
758
|
+
const res = await fetch("/api/brand/license", { method: "DELETE" });
|
|
759
|
+
const data = await res.json();
|
|
760
|
+
|
|
761
|
+
if (data.ok) {
|
|
762
|
+
// Clear brand name and logo from header
|
|
763
|
+
Brand.applyBrandName("OpenClacky");
|
|
764
|
+
Brand.clearBrandCache();
|
|
765
|
+
Brand.applyHeaderLogo();
|
|
766
|
+
// Reset Skills panel state (hide Brand Skills tab, switch to My Skills)
|
|
767
|
+
if (typeof Skills !== "undefined" && Skills.resetAfterUnbind) {
|
|
768
|
+
Skills.resetAfterUnbind();
|
|
769
|
+
}
|
|
770
|
+
// Hide status card, show activation form
|
|
771
|
+
document.getElementById("brand-status-card").style.display = "none";
|
|
772
|
+
document.getElementById("brand-activate-form").style.display = "";
|
|
773
|
+
document.getElementById("settings-license-key").value = "";
|
|
774
|
+
_showBrandResult(true, I18n.t("settings.brand.unbindSuccess"));
|
|
775
|
+
// Reload brand status after a brief delay
|
|
776
|
+
setTimeout(_loadBrand, 800);
|
|
777
|
+
} else {
|
|
778
|
+
_showBrandResult(false, data.error || I18n.t("settings.brand.unbindFailed"));
|
|
779
|
+
}
|
|
780
|
+
} catch (e) {
|
|
781
|
+
_showBrandResult(false, I18n.t("settings.brand.networkRetry"));
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
753
785
|
_initLangBtns();
|
|
754
786
|
|
|
755
787
|
// Re-render model cards when language changes (dynamic HTML, not data-i18n)
|
data/lib/clacky/web/skills.js
CHANGED
|
@@ -33,6 +33,12 @@ const Skills = (() => {
|
|
|
33
33
|
$("skills-tab-my").style.display = tab === "my-skills" ? "" : "none";
|
|
34
34
|
$("skills-tab-brand").style.display = tab === "brand-skills" ? "" : "none";
|
|
35
35
|
|
|
36
|
+
// Toggle visibility of tab-specific controls in the tab bar
|
|
37
|
+
const showSystemLabel = $("label-show-system");
|
|
38
|
+
const refreshBtn = $("btn-refresh-brand-skills");
|
|
39
|
+
if (showSystemLabel) showSystemLabel.style.display = tab === "my-skills" ? "" : "none";
|
|
40
|
+
if (refreshBtn) refreshBtn.style.display = tab === "brand-skills" ? "" : "none";
|
|
41
|
+
|
|
36
42
|
// Lazy-load brand skills when the tab is first opened
|
|
37
43
|
if (tab === "brand-skills" && _brandSkills.length === 0) {
|
|
38
44
|
_loadBrandSkills();
|
|
@@ -359,7 +365,31 @@ const Skills = (() => {
|
|
|
359
365
|
: _skills.filter(s => s.source !== "default");
|
|
360
366
|
|
|
361
367
|
if (visible.length === 0) {
|
|
362
|
-
|
|
368
|
+
const emptyText = I18n.t("skills.empty");
|
|
369
|
+
const createBtnText = I18n.t("skills.empty.createBtn");
|
|
370
|
+
|
|
371
|
+
const emptyWrapper = document.createElement("div");
|
|
372
|
+
emptyWrapper.className = "skills-empty";
|
|
373
|
+
|
|
374
|
+
const emptyTextEl = document.createElement("div");
|
|
375
|
+
emptyTextEl.className = "skills-empty-text";
|
|
376
|
+
emptyTextEl.textContent = emptyText;
|
|
377
|
+
|
|
378
|
+
const createBtn = document.createElement("div");
|
|
379
|
+
createBtn.className = "skills-empty-create-btn";
|
|
380
|
+
createBtn.innerHTML = `
|
|
381
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
382
|
+
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2z"/><path d="M12 8v8"/><path d="M8 12h8"/>
|
|
383
|
+
</svg>
|
|
384
|
+
<span>${escapeHtml(createBtnText)}</span>
|
|
385
|
+
<svg class="skills-empty-create-arrow" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
386
|
+
<path d="M5 12h14"/><path d="M12 5l7 7-7 7"/>
|
|
387
|
+
</svg>`;
|
|
388
|
+
createBtn.addEventListener("click", () => Skills.createInSession("/skill-creator"));
|
|
389
|
+
|
|
390
|
+
emptyWrapper.appendChild(emptyTextEl);
|
|
391
|
+
emptyWrapper.appendChild(createBtn);
|
|
392
|
+
container.appendChild(emptyWrapper);
|
|
363
393
|
} else {
|
|
364
394
|
// System skills first, then custom
|
|
365
395
|
const sorted = [
|
|
@@ -420,8 +450,21 @@ const Skills = (() => {
|
|
|
420
450
|
const refreshBtn = $("btn-refresh-brand-skills");
|
|
421
451
|
if (refreshBtn) {
|
|
422
452
|
refreshBtn.addEventListener("click", async () => {
|
|
453
|
+
// Add spinning animation
|
|
454
|
+
const icon = refreshBtn.querySelector("svg");
|
|
455
|
+
if (icon) {
|
|
456
|
+
icon.classList.add("spinning");
|
|
457
|
+
}
|
|
458
|
+
refreshBtn.disabled = true;
|
|
459
|
+
|
|
423
460
|
_brandSkills = [];
|
|
424
461
|
await _loadBrandSkills();
|
|
462
|
+
|
|
463
|
+
// Remove spinning animation
|
|
464
|
+
if (icon) {
|
|
465
|
+
icon.classList.remove("spinning");
|
|
466
|
+
}
|
|
467
|
+
refreshBtn.disabled = false;
|
|
425
468
|
});
|
|
426
469
|
}
|
|
427
470
|
|
|
@@ -513,6 +556,23 @@ const Skills = (() => {
|
|
|
513
556
|
_switchTab("brand-skills");
|
|
514
557
|
},
|
|
515
558
|
|
|
559
|
+
/** Reset the skills panel back to My Skills tab and clear brand data.
|
|
560
|
+
* Called after license unbind so the user is not left on Brand Skills tab.
|
|
561
|
+
*/
|
|
562
|
+
resetAfterUnbind() {
|
|
563
|
+
_brandSkills = [];
|
|
564
|
+
_brandActivated = false;
|
|
565
|
+
_activeTab = "my-skills";
|
|
566
|
+
// Hide the Brand Skills tab since there is no active license
|
|
567
|
+
const brandTab = $("tab-brand-skills");
|
|
568
|
+
if (brandTab) brandTab.style.display = "none";
|
|
569
|
+
// If the panel is currently visible, switch to My Skills immediately
|
|
570
|
+
const panel = $("skills-panel");
|
|
571
|
+
if (panel && panel.style.display !== "none") {
|
|
572
|
+
_switchTab("my-skills");
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
|
|
516
576
|
// ── Import bar ────────────────────────────────────────────────────────
|
|
517
577
|
|
|
518
578
|
/** Toggle the inline import bar below the My Skills header.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.32
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
@@ -305,7 +305,10 @@ files:
|
|
|
305
305
|
- lib/clacky/agent/message_compressor.rb
|
|
306
306
|
- lib/clacky/agent/message_compressor_helper.rb
|
|
307
307
|
- lib/clacky/agent/session_serializer.rb
|
|
308
|
+
- lib/clacky/agent/skill_auto_creator.rb
|
|
309
|
+
- lib/clacky/agent/skill_evolution.rb
|
|
308
310
|
- lib/clacky/agent/skill_manager.rb
|
|
311
|
+
- lib/clacky/agent/skill_reflector.rb
|
|
309
312
|
- lib/clacky/agent/system_prompt_builder.rb
|
|
310
313
|
- lib/clacky/agent/time_machine.rb
|
|
311
314
|
- lib/clacky/agent/tool_executor.rb
|