openclacky 0.9.35 → 0.9.37
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 +25 -0
- data/lib/clacky/agent/session_serializer.rb +6 -1
- data/lib/clacky/agent/skill_manager.rb +20 -8
- data/lib/clacky/agent/tool_executor.rb +1 -0
- data/lib/clacky/agent.rb +22 -17
- data/lib/clacky/agent_config.rb +166 -40
- data/lib/clacky/cli.rb +32 -13
- data/lib/clacky/message_history.rb +43 -2
- data/lib/clacky/providers.rb +21 -0
- data/lib/clacky/server/http_server.rb +208 -83
- data/lib/clacky/server/session_registry.rb +32 -5
- data/lib/clacky/tools/edit.rb +11 -1
- data/lib/clacky/tools/file_reader.rb +19 -3
- data/lib/clacky/tools/glob.rb +1 -0
- data/lib/clacky/tools/grep.rb +12 -3
- data/lib/clacky/tools/terminal.rb +154 -15
- data/lib/clacky/ui2/ui_controller.rb +16 -7
- data/lib/clacky/utils/model_pricing.rb +45 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +75 -149
- data/lib/clacky/web/app.js +23 -17
- data/lib/clacky/web/i18n.js +10 -0
- data/lib/clacky/web/index.html +8 -18
- data/lib/clacky/web/sessions.js +92 -57
- metadata +1 -1
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -46,6 +46,11 @@ const I18n = (() => {
|
|
|
46
46
|
"chat.retry": "Retry",
|
|
47
47
|
"chat.copy": "Copy",
|
|
48
48
|
"chat.copied": "Copied",
|
|
49
|
+
"chat.empty.title": "Start the conversation",
|
|
50
|
+
"chat.empty.subtitle": "Ask anything, or use a skill to get going.",
|
|
51
|
+
"chat.empty.tip1": "Type / to browse skills",
|
|
52
|
+
"chat.empty.tip2": "Attach images, PDFs, or docs for context",
|
|
53
|
+
"chat.empty.tip3": "Shift+Enter for a new line",
|
|
49
54
|
|
|
50
55
|
// ── Session list ──
|
|
51
56
|
"sessions.empty": "No sessions yet",
|
|
@@ -422,6 +427,11 @@ const I18n = (() => {
|
|
|
422
427
|
"chat.retry": "重试",
|
|
423
428
|
"chat.copy": "复制",
|
|
424
429
|
"chat.copied": "已复制",
|
|
430
|
+
"chat.empty.title": "开始新的对话",
|
|
431
|
+
"chat.empty.subtitle": "直接提问,或用一个 Skill 启动。",
|
|
432
|
+
"chat.empty.tip1": "输入 / 浏览 Skill",
|
|
433
|
+
"chat.empty.tip2": "可粘贴或拖入图片、PDF、文档",
|
|
434
|
+
"chat.empty.tip3": "Shift+Enter 换行",
|
|
425
435
|
|
|
426
436
|
// ── Session list ──
|
|
427
437
|
"sessions.empty": "暂无对话",
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -234,24 +234,14 @@
|
|
|
234
234
|
|
|
235
235
|
<!-- Chat panel -->
|
|
236
236
|
<div id="chat-panel" style="display:none">
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
</
|
|
244
|
-
|
|
245
|
-
<span id="chat-title">Session</span>
|
|
246
|
-
<span id="chat-subtitle"></span>
|
|
247
|
-
</span>
|
|
248
|
-
<span id="chat-status" class="status-idle">idle</span>
|
|
249
|
-
<button id="btn-delete-session" class="btn-delete-session" title="Delete session">
|
|
250
|
-
<svg 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">
|
|
251
|
-
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
|
252
|
-
</svg>
|
|
253
|
-
</button>
|
|
254
|
-
</header>
|
|
237
|
+
<!-- Mobile-only floating back button: shown inside a session on small screens.
|
|
238
|
+
Desktop: hidden. All other session info (title, status, dir, delete) is
|
|
239
|
+
provided by the bottom #session-info-bar. -->
|
|
240
|
+
<button id="btn-back-mobile" class="chat-back-btn chat-back-floating" title="Back">
|
|
241
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
242
|
+
<path d="M15 18l-6-6 6-6"/>
|
|
243
|
+
</svg>
|
|
244
|
+
</button>
|
|
255
245
|
<div id="messages"></div>
|
|
256
246
|
<!-- New message notification banner -->
|
|
257
247
|
<div id="new-message-banner" class="new-message-banner" style="display:none">
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -183,6 +183,75 @@ const Sessions = (() => {
|
|
|
183
183
|
banner.style.display = "none";
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// ── Empty-state hint ──────────────────────────────────────────────────
|
|
187
|
+
//
|
|
188
|
+
// Shows a small centered hint inside #messages when the message list is
|
|
189
|
+
// empty (e.g. just-created session with no history). Uses a MutationObserver
|
|
190
|
+
// so we don't have to instrument every append/clear call site.
|
|
191
|
+
|
|
192
|
+
const _EMPTY_HINT_ID = "chat-empty-hint";
|
|
193
|
+
|
|
194
|
+
function _buildEmptyHintHtml() {
|
|
195
|
+
const title = I18n.t("chat.empty.title");
|
|
196
|
+
const subtitle = I18n.t("chat.empty.subtitle");
|
|
197
|
+
const tip1 = I18n.t("chat.empty.tip1");
|
|
198
|
+
const tip2 = I18n.t("chat.empty.tip2");
|
|
199
|
+
const tip3 = I18n.t("chat.empty.tip3");
|
|
200
|
+
return `
|
|
201
|
+
<div class="chat-empty-icon" aria-hidden="true">
|
|
202
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
203
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
204
|
+
</svg>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="chat-empty-title">${escapeHtml(title)}</div>
|
|
207
|
+
<div class="chat-empty-subtitle">${escapeHtml(subtitle)}</div>
|
|
208
|
+
<ul class="chat-empty-tips">
|
|
209
|
+
<li>${escapeHtml(tip1)}</li>
|
|
210
|
+
<li>${escapeHtml(tip2)}</li>
|
|
211
|
+
<li>${escapeHtml(tip3)}</li>
|
|
212
|
+
</ul>
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function _updateEmptyHint() {
|
|
217
|
+
const messages = $("messages");
|
|
218
|
+
if (!messages) return;
|
|
219
|
+
// Check if there's any real content besides the hint itself
|
|
220
|
+
const hasReal = Array.from(messages.children).some(
|
|
221
|
+
(el) => el.id !== _EMPTY_HINT_ID
|
|
222
|
+
);
|
|
223
|
+
const existing = document.getElementById(_EMPTY_HINT_ID);
|
|
224
|
+
// While history is still loading, don't flash the hint — wait until the
|
|
225
|
+
// first fetch completes so we know whether the session is actually empty.
|
|
226
|
+
const loading = !!(_activeId && _historyState[_activeId] && _historyState[_activeId].loading);
|
|
227
|
+
if (hasReal || loading) {
|
|
228
|
+
if (existing) existing.remove();
|
|
229
|
+
} else {
|
|
230
|
+
if (!existing) {
|
|
231
|
+
const el = document.createElement("div");
|
|
232
|
+
el.id = _EMPTY_HINT_ID;
|
|
233
|
+
el.className = "chat-empty-hint";
|
|
234
|
+
el.innerHTML = _buildEmptyHintHtml();
|
|
235
|
+
messages.appendChild(el);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _initEmptyHint() {
|
|
241
|
+
const messages = $("messages");
|
|
242
|
+
if (!messages) return;
|
|
243
|
+
// Re-evaluate whenever children change (append/insertBefore/innerHTML="")
|
|
244
|
+
const observer = new MutationObserver(() => _updateEmptyHint());
|
|
245
|
+
observer.observe(messages, { childList: true });
|
|
246
|
+
// Re-render hint text on language change
|
|
247
|
+
document.addEventListener("langchange", () => {
|
|
248
|
+
const existing = document.getElementById(_EMPTY_HINT_ID);
|
|
249
|
+
if (existing) existing.innerHTML = _buildEmptyHintHtml();
|
|
250
|
+
});
|
|
251
|
+
// Initial paint
|
|
252
|
+
_updateEmptyHint();
|
|
253
|
+
}
|
|
254
|
+
|
|
186
255
|
function _initNewMessageBanner() {
|
|
187
256
|
const banner = $("new-message-banner");
|
|
188
257
|
const messages = $("messages");
|
|
@@ -640,6 +709,9 @@ const Sessions = (() => {
|
|
|
640
709
|
}
|
|
641
710
|
} finally {
|
|
642
711
|
state.loading = false;
|
|
712
|
+
// After loading finishes, re-evaluate the empty-state hint in case
|
|
713
|
+
// the session is genuinely empty (no events + no existing DOM content).
|
|
714
|
+
if (id === _activeId) _updateEmptyHint();
|
|
643
715
|
}
|
|
644
716
|
}
|
|
645
717
|
|
|
@@ -891,6 +963,7 @@ const Sessions = (() => {
|
|
|
891
963
|
// ── Init ──────────────────────────────────────────────────────────────
|
|
892
964
|
init() {
|
|
893
965
|
_initNewMessageBanner();
|
|
966
|
+
_initEmptyHint();
|
|
894
967
|
// Re-render session list (badges/labels) when the user switches language
|
|
895
968
|
document.addEventListener("langchange", () => Sessions.renderList());
|
|
896
969
|
// Browsers block file:// navigation from http:// pages. Intercept clicks on
|
|
@@ -954,8 +1027,14 @@ const Sessions = (() => {
|
|
|
954
1027
|
Sessions.renderList();
|
|
955
1028
|
|
|
956
1029
|
try {
|
|
957
|
-
// Cursor: oldest created_at in the current list
|
|
1030
|
+
// Cursor: oldest created_at in the current list, EXCLUDING pinned
|
|
1031
|
+
// sessions. The backend always returns ALL pinned sessions on the
|
|
1032
|
+
// first page (they bypass pagination), so their created_at is
|
|
1033
|
+
// irrelevant for cursor calculation. Including them here would
|
|
1034
|
+
// cause the cursor to jump too far back and skip sessions between
|
|
1035
|
+
// the oldest pinned one and the real last-loaded non-pinned row.
|
|
958
1036
|
const oldest = _sessions.reduce((min, s) => {
|
|
1037
|
+
if (s.pinned) return min; // ignore pinned
|
|
959
1038
|
if (!s.created_at) return min;
|
|
960
1039
|
return (!min || s.created_at < min) ? s.created_at : min;
|
|
961
1040
|
}, null);
|
|
@@ -1260,8 +1339,8 @@ const Sessions = (() => {
|
|
|
1260
1339
|
Sessions.patch(sessionId, { name: newName });
|
|
1261
1340
|
Sessions.renderList();
|
|
1262
1341
|
if (sessionId === Sessions.activeId) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1342
|
+
// chat-header was removed — no title element to update here.
|
|
1343
|
+
// Sidebar re-renders with the new name above.
|
|
1265
1344
|
}
|
|
1266
1345
|
} else {
|
|
1267
1346
|
console.error("Rename failed:", await res.text());
|
|
@@ -1399,64 +1478,20 @@ const Sessions = (() => {
|
|
|
1399
1478
|
},
|
|
1400
1479
|
|
|
1401
1480
|
updateStatusBar(status) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
$("chat-status").className = "status-error";
|
|
1407
|
-
} else {
|
|
1408
|
-
$("chat-status").className = "status-idle";
|
|
1409
|
-
}
|
|
1410
|
-
$("btn-interrupt").style.display = status === "running" ? "" : "none";
|
|
1481
|
+
// chat-header was removed; status text is now shown in the bottom session-info-bar (#sib-status).
|
|
1482
|
+
// Here we only update the interrupt button visibility.
|
|
1483
|
+
const interrupt = $("btn-interrupt");
|
|
1484
|
+
if (interrupt) interrupt.style.display = status === "running" ? "" : "none";
|
|
1411
1485
|
},
|
|
1412
1486
|
|
|
1413
1487
|
/**
|
|
1414
|
-
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
* this session is without glancing down at the status bar.
|
|
1488
|
+
* No-op: the chat header element (#chat-header) was removed. All session
|
|
1489
|
+
* metadata (title, source, working dir, status) is now shown in the
|
|
1490
|
+
* sidebar and the bottom #session-info-bar. Kept as a stub so existing
|
|
1491
|
+
* call sites don't need to be updated.
|
|
1419
1492
|
*/
|
|
1420
|
-
updateChatHeader(
|
|
1421
|
-
|
|
1422
|
-
const subEl = $("chat-subtitle");
|
|
1423
|
-
if (!titleEl || !subEl) return;
|
|
1424
|
-
|
|
1425
|
-
if (!s) {
|
|
1426
|
-
titleEl.textContent = "";
|
|
1427
|
-
subEl.innerHTML = "";
|
|
1428
|
-
subEl.style.display = "none";
|
|
1429
|
-
return;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
titleEl.textContent = s.name || _relativeTime(s.created_at);
|
|
1433
|
-
|
|
1434
|
-
// Build subtitle pieces. No emoji — keeps the UI clean and avoids
|
|
1435
|
-
// the "AI-generated" feel that overuse of emoji creates.
|
|
1436
|
-
const parts = [];
|
|
1437
|
-
let sourceLabel = "";
|
|
1438
|
-
if (s.source === "cron") sourceLabel = I18n.t("sessions.badge.cron");
|
|
1439
|
-
else if (s.source === "channel") sourceLabel = I18n.t("sessions.badge.channel");
|
|
1440
|
-
else if (s.source === "setup") sourceLabel = I18n.t("sessions.badge.setup");
|
|
1441
|
-
|
|
1442
|
-
if (sourceLabel) {
|
|
1443
|
-
parts.push(
|
|
1444
|
-
`<span class="chat-sub-source">${escapeHtml(sourceLabel)}</span>`
|
|
1445
|
-
);
|
|
1446
|
-
}
|
|
1447
|
-
if (s.working_dir) {
|
|
1448
|
-
parts.push(
|
|
1449
|
-
`<span class="chat-sub-dir" title="${escapeHtml(s.working_dir)}">${escapeHtml(s.working_dir)}</span>`
|
|
1450
|
-
);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
if (parts.length === 0) {
|
|
1454
|
-
subEl.innerHTML = "";
|
|
1455
|
-
subEl.style.display = "none";
|
|
1456
|
-
} else {
|
|
1457
|
-
subEl.innerHTML = parts.join(`<span class="chat-sub-sep">·</span>`);
|
|
1458
|
-
subEl.style.display = "";
|
|
1459
|
-
}
|
|
1493
|
+
updateChatHeader(_s) {
|
|
1494
|
+
// intentionally empty
|
|
1460
1495
|
},
|
|
1461
1496
|
|
|
1462
1497
|
/** Update the session info bar below the chat header with current session metadata. */
|